733 lines
29 KiB
TypeScript
733 lines
29 KiB
TypeScript
/**
|
|
* Bot Detail/Edit Page
|
|
*
|
|
* View and edit bot configuration, manage sources, view logs.
|
|
*
|
|
* Requirements: 1.3, 4.6, 8.2
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useParams, useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import { ArrowLeftIcon } from '@/components/Icons';
|
|
import { Bot, Play, Pause, Rss, Activity, Settings, Sparkles, Clock, Trash2, Pencil } from 'lucide-react';
|
|
import { useToast } from '@/lib/contexts/ToastContext';
|
|
|
|
export default function BotDetailPage() {
|
|
const params = useParams();
|
|
const router = useRouter();
|
|
const { showToast } = useToast();
|
|
const botId = params.id as string;
|
|
const [bot, setBot] = useState<any>(null);
|
|
const [sources, setSources] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [actionLoading, setActionLoading] = useState(false);
|
|
const [showAddSource, setShowAddSource] = useState(false);
|
|
const [newSource, setNewSource] = useState({
|
|
type: 'rss' as 'rss' | 'reddit' | 'news_api' | 'brave_news',
|
|
url: '',
|
|
subreddit: '',
|
|
apiKey: '',
|
|
// Brave News config
|
|
braveQuery: '',
|
|
braveFreshness: 'pw' as 'pd' | 'pw' | 'pm' | 'py',
|
|
braveCountry: '',
|
|
// News API config
|
|
newsProvider: 'newsapi' as 'newsapi' | 'gnews' | 'newsdata',
|
|
newsQuery: '',
|
|
newsCategory: '',
|
|
newsCountry: '',
|
|
newsLanguage: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchBot();
|
|
fetchSources();
|
|
}, [botId]);
|
|
|
|
const fetchBot = async () => {
|
|
try {
|
|
const response = await fetch(`/api/bots/${botId}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setBot(data.bot);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch bot:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchSources = async () => {
|
|
try {
|
|
const response = await fetch(`/api/bots/${botId}/sources`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setSources(data.sources || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch sources:', error);
|
|
}
|
|
};
|
|
|
|
const handleTriggerPost = async () => {
|
|
setActionLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/bots/${botId}/post`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
});
|
|
if (response.ok) {
|
|
showToast('Post triggered successfully!', 'success');
|
|
fetchBot();
|
|
} else {
|
|
const data = await response.json();
|
|
showToast(`Failed to trigger post: ${data.error || 'Unknown error'}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Failed to trigger post', 'error');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleToggleActive = async () => {
|
|
setActionLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/bots/${botId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ isActive: !bot.isActive }),
|
|
});
|
|
if (response.ok) {
|
|
fetchBot();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to toggle bot:', error);
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAddSource = async () => {
|
|
setActionLoading(true);
|
|
try {
|
|
let url = newSource.url;
|
|
const payload: Record<string, unknown> = {
|
|
type: newSource.type,
|
|
apiKey: newSource.apiKey || undefined,
|
|
};
|
|
|
|
// Build URL and config based on type
|
|
if (newSource.type === 'brave_news') {
|
|
if (!newSource.braveQuery || !newSource.apiKey) {
|
|
showToast('Search query and API key are required for Brave News', 'error');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
const braveUrl = new URL('https://api.search.brave.com/res/v1/news/search');
|
|
braveUrl.searchParams.set('q', newSource.braveQuery);
|
|
if (newSource.braveFreshness) braveUrl.searchParams.set('freshness', newSource.braveFreshness);
|
|
if (newSource.braveCountry) braveUrl.searchParams.set('country', newSource.braveCountry);
|
|
url = braveUrl.toString();
|
|
payload.braveNewsConfig = {
|
|
query: newSource.braveQuery,
|
|
freshness: newSource.braveFreshness,
|
|
country: newSource.braveCountry || undefined,
|
|
};
|
|
} else if (newSource.type === 'news_api') {
|
|
if (!newSource.newsQuery || !newSource.apiKey) {
|
|
showToast('Search query and API key are required for News API', 'error');
|
|
setActionLoading(false);
|
|
return;
|
|
}
|
|
let baseUrl: string;
|
|
const params = new URLSearchParams();
|
|
switch (newSource.newsProvider) {
|
|
case 'gnews':
|
|
baseUrl = 'https://gnews.io/api/v4/search';
|
|
params.set('q', newSource.newsQuery);
|
|
if (newSource.newsCountry) params.set('country', newSource.newsCountry);
|
|
if (newSource.newsLanguage) params.set('lang', newSource.newsLanguage);
|
|
if (newSource.newsCategory) params.set('topic', newSource.newsCategory);
|
|
break;
|
|
case 'newsdata':
|
|
baseUrl = 'https://newsdata.io/api/1/news';
|
|
params.set('q', newSource.newsQuery);
|
|
if (newSource.newsCountry) params.set('country', newSource.newsCountry);
|
|
if (newSource.newsLanguage) params.set('language', newSource.newsLanguage);
|
|
if (newSource.newsCategory) params.set('category', newSource.newsCategory);
|
|
break;
|
|
default:
|
|
baseUrl = 'https://newsapi.org/v2/everything';
|
|
params.set('q', newSource.newsQuery);
|
|
if (newSource.newsLanguage) params.set('language', newSource.newsLanguage);
|
|
}
|
|
url = `${baseUrl}?${params.toString()}`;
|
|
payload.newsApiConfig = {
|
|
provider: newSource.newsProvider,
|
|
query: newSource.newsQuery,
|
|
category: newSource.newsCategory || undefined,
|
|
country: newSource.newsCountry || undefined,
|
|
language: newSource.newsLanguage || undefined,
|
|
};
|
|
} else if (newSource.type === 'reddit') {
|
|
payload.subreddit = newSource.subreddit || undefined;
|
|
}
|
|
|
|
payload.url = url;
|
|
|
|
const response = await fetch(`/api/bots/${botId}/sources`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (response.ok) {
|
|
setShowAddSource(false);
|
|
setNewSource({
|
|
type: 'rss',
|
|
url: '',
|
|
subreddit: '',
|
|
apiKey: '',
|
|
braveQuery: '',
|
|
braveFreshness: 'pw',
|
|
braveCountry: '',
|
|
newsProvider: 'newsapi',
|
|
newsQuery: '',
|
|
newsCategory: '',
|
|
newsCountry: '',
|
|
newsLanguage: '',
|
|
});
|
|
fetchSources();
|
|
} else {
|
|
const data = await response.json();
|
|
showToast(`Failed to add source: ${data.error || 'Unknown error'}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Failed to add source', 'error');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleFetchSource = async (sourceId: string) => {
|
|
setActionLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/bots/${botId}/sources/${sourceId}/fetch`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({}),
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
showToast(`Fetched ${data.itemsFetched || 0} items successfully!`, 'success');
|
|
fetchSources();
|
|
} else {
|
|
const data = await response.json();
|
|
showToast(`Failed to fetch content: ${data.error || 'Unknown error'}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Failed to fetch content', 'error');
|
|
} finally {
|
|
setActionLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '24px 16px 64px' }}>
|
|
<div style={{ textAlign: 'center', padding: '48px 24px', color: 'var(--foreground-tertiary)' }}>
|
|
Loading...
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!bot) {
|
|
return (
|
|
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '24px 16px 64px' }}>
|
|
<div className="card" style={{ padding: '48px 24px', textAlign: 'center' }}>
|
|
<p style={{ color: 'var(--foreground-tertiary)' }}>Bot not found</p>
|
|
<Link href="/settings/bots" className="btn" style={{ marginTop: '16px' }}>
|
|
Back to Bots
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const scheduleConfig = typeof bot.scheduleConfig === 'string'
|
|
? JSON.parse(bot.scheduleConfig)
|
|
: bot.scheduleConfig || null;
|
|
const personalityConfig = typeof bot.personalityConfig === 'string'
|
|
? JSON.parse(bot.personalityConfig)
|
|
: bot.personalityConfig || {};
|
|
|
|
return (
|
|
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '24px 16px 64px' }}>
|
|
<header style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '32px' }}>
|
|
<Link href="/settings/bots" style={{ color: 'var(--foreground)' }}>
|
|
<ArrowLeftIcon />
|
|
</Link>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
|
<h1 style={{ fontSize: '24px', fontWeight: 700 }}>{bot.name}</h1>
|
|
{bot.autonomousMode && (
|
|
<span style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
fontSize: '11px',
|
|
padding: '4px 10px',
|
|
borderRadius: 'var(--radius-full)',
|
|
background: 'var(--accent-muted)',
|
|
color: 'var(--accent)',
|
|
}}>
|
|
<Sparkles size={12} />
|
|
Autonomous
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p style={{ fontSize: '14px', color: 'var(--foreground-tertiary)' }}>
|
|
@{bot.handle}
|
|
</p>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
{bot.isSuspended ? (
|
|
<span className="status-pill suspended">Suspended</span>
|
|
) : bot.isActive ? (
|
|
<span className="status-pill active">Active</span>
|
|
) : (
|
|
<span className="status-pill">Inactive</span>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="card" style={{ padding: '20px', marginBottom: '16px' }}>
|
|
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Play size={18} />
|
|
Quick Actions
|
|
</h2>
|
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
<button
|
|
onClick={handleTriggerPost}
|
|
disabled={actionLoading || bot.isSuspended}
|
|
className="btn btn-primary"
|
|
>
|
|
<Play size={16} />
|
|
Trigger Post
|
|
</button>
|
|
<button
|
|
onClick={handleToggleActive}
|
|
disabled={actionLoading || bot.isSuspended}
|
|
className="btn"
|
|
>
|
|
{bot.isActive ? <Pause size={16} /> : <Play size={16} />}
|
|
{bot.isActive ? 'Deactivate' : 'Activate'}
|
|
</button>
|
|
<Link
|
|
href={`/settings/bots/${botId}/edit`}
|
|
className="btn"
|
|
>
|
|
<Pencil size={16} />
|
|
Edit Bot
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bot Info */}
|
|
<div className="card" style={{ padding: '20px', marginBottom: '16px' }}>
|
|
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Bot size={18} />
|
|
Bot Information
|
|
</h2>
|
|
<div style={{ display: 'grid', gap: '12px' }}>
|
|
{bot.bio && (
|
|
<div>
|
|
<div style={{ fontSize: '13px', color: 'var(--foreground-tertiary)', marginBottom: '4px' }}>
|
|
Bio
|
|
</div>
|
|
<div style={{ fontSize: '14px', color: 'var(--foreground-secondary)' }}>
|
|
{bot.bio}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '12px' }}>
|
|
<div>
|
|
<div style={{ fontSize: '13px', color: 'var(--foreground-tertiary)', marginBottom: '4px' }}>
|
|
Last Post
|
|
</div>
|
|
<div style={{ fontSize: '14px' }}>
|
|
{bot.lastPostAt ? new Date(bot.lastPostAt).toLocaleString() : 'Never'}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: '13px', color: 'var(--foreground-tertiary)', marginBottom: '4px' }}>
|
|
Created
|
|
</div>
|
|
<div style={{ fontSize: '14px' }}>
|
|
{new Date(bot.createdAt).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: '13px', color: 'var(--foreground-tertiary)', marginBottom: '4px' }}>
|
|
LLM Provider
|
|
</div>
|
|
<div style={{ fontSize: '14px' }}>
|
|
{bot.llmProvider} / {bot.llmModel}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Schedule */}
|
|
{bot.autonomousMode && scheduleConfig && (
|
|
<div className="card" style={{ padding: '20px', marginBottom: '16px' }}>
|
|
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Clock size={18} />
|
|
Posting Schedule
|
|
</h2>
|
|
<div style={{ fontSize: '14px', color: 'var(--foreground-secondary)' }}>
|
|
{scheduleConfig.type === 'interval' && scheduleConfig.intervalMinutes
|
|
? `Posts every ${scheduleConfig.intervalMinutes} minutes`
|
|
: 'Custom schedule configured'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Personality */}
|
|
<div className="card" style={{ padding: '20px', marginBottom: '16px' }}>
|
|
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Sparkles size={18} />
|
|
Personality
|
|
</h2>
|
|
<div style={{
|
|
fontSize: '13px',
|
|
color: 'var(--foreground-secondary)',
|
|
background: 'var(--background-tertiary)',
|
|
padding: '12px',
|
|
borderRadius: 'var(--radius-md)',
|
|
whiteSpace: 'pre-wrap',
|
|
fontFamily: 'monospace',
|
|
}}>
|
|
{personalityConfig?.systemPrompt || 'No system prompt configured'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Sources */}
|
|
<div className="card" style={{ padding: '20px', marginBottom: '16px' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
|
<h2 style={{ fontSize: '16px', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Rss size={18} />
|
|
Content Sources ({sources.length})
|
|
</h2>
|
|
<button
|
|
onClick={() => setShowAddSource(!showAddSource)}
|
|
className="btn btn-sm btn-primary"
|
|
>
|
|
{showAddSource ? 'Cancel' : '+ Add Source'}
|
|
</button>
|
|
</div>
|
|
|
|
{showAddSource && (
|
|
<div className="card" style={{ padding: '16px', marginBottom: '16px', background: 'var(--background-tertiary)' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Source Type
|
|
</label>
|
|
<select
|
|
value={newSource.type}
|
|
onChange={(e) => setNewSource({ ...newSource, type: e.target.value as typeof newSource.type })}
|
|
className="input"
|
|
>
|
|
<option value="rss">RSS Feed</option>
|
|
<option value="reddit">Reddit</option>
|
|
<option value="brave_news">Brave News Search</option>
|
|
<option value="news_api">News API (Advanced)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{newSource.type === 'rss' && (
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
RSS Feed URL
|
|
</label>
|
|
<input
|
|
type="url"
|
|
value={newSource.url}
|
|
onChange={(e) => setNewSource({ ...newSource, url: e.target.value })}
|
|
className="input"
|
|
placeholder="https://example.com/feed.xml"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{newSource.type === 'reddit' && (
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Subreddit
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newSource.subreddit}
|
|
onChange={(e) => setNewSource({ ...newSource, subreddit: e.target.value, url: `https://reddit.com/r/${e.target.value}` })}
|
|
className="input"
|
|
placeholder="technology"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{newSource.type === 'brave_news' && (
|
|
<>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Search Query
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newSource.braveQuery}
|
|
onChange={(e) => setNewSource({ ...newSource, braveQuery: e.target.value })}
|
|
className="input"
|
|
placeholder="AI technology, climate change, etc."
|
|
/>
|
|
<p style={{ fontSize: '12px', color: 'var(--foreground-tertiary)', marginTop: '4px' }}>
|
|
Use quotes for exact phrases, minus to exclude terms
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Freshness
|
|
</label>
|
|
<select
|
|
value={newSource.braveFreshness}
|
|
onChange={(e) => setNewSource({ ...newSource, braveFreshness: e.target.value as typeof newSource.braveFreshness })}
|
|
className="input"
|
|
>
|
|
<option value="pd">Last 24 hours</option>
|
|
<option value="pw">Last 7 days</option>
|
|
<option value="pm">Last 31 days</option>
|
|
<option value="py">Last year</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Country (optional)
|
|
</label>
|
|
<select
|
|
value={newSource.braveCountry}
|
|
onChange={(e) => setNewSource({ ...newSource, braveCountry: e.target.value })}
|
|
className="input"
|
|
>
|
|
<option value="">All countries</option>
|
|
<option value="US">United States</option>
|
|
<option value="GB">United Kingdom</option>
|
|
<option value="CA">Canada</option>
|
|
<option value="AU">Australia</option>
|
|
<option value="DE">Germany</option>
|
|
<option value="FR">France</option>
|
|
<option value="JP">Japan</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Brave API Key
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={newSource.apiKey}
|
|
onChange={(e) => setNewSource({ ...newSource, apiKey: e.target.value })}
|
|
className="input"
|
|
placeholder="Your Brave Search API key"
|
|
/>
|
|
<p style={{ fontSize: '12px', color: 'var(--foreground-tertiary)', marginTop: '4px' }}>
|
|
Get your API key at <a href="https://brave.com/search/api/" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--accent)' }}>brave.com/search/api</a>
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{newSource.type === 'news_api' && (
|
|
<>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
News Provider
|
|
</label>
|
|
<select
|
|
value={newSource.newsProvider}
|
|
onChange={(e) => setNewSource({ ...newSource, newsProvider: e.target.value as typeof newSource.newsProvider })}
|
|
className="input"
|
|
>
|
|
<option value="newsapi">NewsAPI.org</option>
|
|
<option value="gnews">GNews.io</option>
|
|
<option value="newsdata">NewsData.io</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Search Keywords
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newSource.newsQuery}
|
|
onChange={(e) => setNewSource({ ...newSource, newsQuery: e.target.value })}
|
|
className="input"
|
|
placeholder="technology, AI, startups"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Category (optional)
|
|
</label>
|
|
<select
|
|
value={newSource.newsCategory}
|
|
onChange={(e) => setNewSource({ ...newSource, newsCategory: e.target.value })}
|
|
className="input"
|
|
>
|
|
<option value="">All categories</option>
|
|
<option value="technology">Technology</option>
|
|
<option value="business">Business</option>
|
|
<option value="science">Science</option>
|
|
<option value="health">Health</option>
|
|
<option value="sports">Sports</option>
|
|
<option value="entertainment">Entertainment</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
Country (optional)
|
|
</label>
|
|
<select
|
|
value={newSource.newsCountry}
|
|
onChange={(e) => setNewSource({ ...newSource, newsCountry: e.target.value })}
|
|
className="input"
|
|
>
|
|
<option value="">All countries</option>
|
|
<option value="us">United States</option>
|
|
<option value="gb">United Kingdom</option>
|
|
<option value="ca">Canada</option>
|
|
<option value="au">Australia</option>
|
|
<option value="de">Germany</option>
|
|
<option value="fr">France</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label style={{ display: 'block', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }}>
|
|
API Key
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={newSource.apiKey}
|
|
onChange={(e) => setNewSource({ ...newSource, apiKey: e.target.value })}
|
|
className="input"
|
|
placeholder="Your API key"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleAddSource}
|
|
disabled={
|
|
actionLoading ||
|
|
(newSource.type === 'rss' && !newSource.url) ||
|
|
(newSource.type === 'reddit' && !newSource.subreddit) ||
|
|
(newSource.type === 'brave_news' && (!newSource.braveQuery || !newSource.apiKey)) ||
|
|
(newSource.type === 'news_api' && (!newSource.newsQuery || !newSource.apiKey))
|
|
}
|
|
className="btn btn-primary"
|
|
>
|
|
{actionLoading ? 'Adding...' : 'Add Source'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{sources.length === 0 ? (
|
|
<p style={{ fontSize: '13px', color: 'var(--foreground-tertiary)', textAlign: 'center', padding: '24px 0' }}>
|
|
No content sources configured. Add a source to enable posting.
|
|
</p>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
{sources.map((source: any) => {
|
|
let displayName = '';
|
|
try {
|
|
if (source.type === 'brave_news') {
|
|
const urlObj = new URL(source.url);
|
|
displayName = urlObj.searchParams.get('q') || 'Brave News';
|
|
} else if (source.subreddit) {
|
|
displayName = source.subreddit;
|
|
} else if (source.url) {
|
|
displayName = new URL(source.url).hostname;
|
|
} else {
|
|
displayName = 'Unknown';
|
|
}
|
|
} catch {
|
|
displayName = source.url || 'Unknown';
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={source.id}
|
|
style={{
|
|
padding: '12px',
|
|
background: 'var(--background-tertiary)',
|
|
borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--border)',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontSize: '13px', fontWeight: 500, marginBottom: '4px' }}>
|
|
{source.type === 'brave_news' ? 'BRAVE NEWS' : source.type.toUpperCase()} - {displayName}
|
|
</div>
|
|
<div style={{ fontSize: '12px', color: 'var(--foreground-tertiary)' }}>
|
|
{source.lastFetchAt ? `Last fetched: ${new Date(source.lastFetchAt).toLocaleString()}` : 'Never fetched'}
|
|
{source.lastError && <span style={{ color: 'var(--error)' }}> • Error: {source.lastError}</span>}
|
|
</div>
|
|
</div>
|
|
<span className={`status-pill ${source.isActive ? 'active' : ''}`}>
|
|
{source.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Danger Zone */}
|
|
<div className="card" style={{ padding: '20px', borderColor: 'var(--error)' }}>
|
|
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', color: 'var(--error)', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Trash2 size={18} />
|
|
Danger Zone
|
|
</h2>
|
|
<p style={{ fontSize: '13px', color: 'var(--foreground-secondary)', marginBottom: '12px' }}>
|
|
Deleting a bot is permanent and cannot be undone. All associated data will be removed.
|
|
</p>
|
|
<button
|
|
onClick={() => {
|
|
if (confirm(`Are you sure you want to delete ${bot.name}? This cannot be undone.`)) {
|
|
fetch(`/api/bots/${botId}`, { method: 'DELETE' })
|
|
.then(() => router.push('/settings/bots'))
|
|
.catch(() => showToast('Failed to delete bot', 'error'));
|
|
}
|
|
}}
|
|
className="btn"
|
|
style={{ color: 'var(--error)', borderColor: 'var(--error)' }}
|
|
>
|
|
<Trash2 size={16} />
|
|
Delete Bot
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|