/** * 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(null); const [sources, setSources] = useState([]); 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 = { 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 (
Loading...
); } if (!bot) { return (

Bot not found

Back to Bots
); } 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 (

{bot.name}

{bot.autonomousMode && ( Autonomous )}

@{bot.handle}

{bot.isSuspended ? ( Suspended ) : bot.isActive ? ( Active ) : ( Inactive )}
{/* Quick Actions */}

Quick Actions

Edit Bot
{/* Bot Info */}

Bot Information

{bot.bio && (
Bio
{bot.bio}
)}
Last Post
{bot.lastPostAt ? new Date(bot.lastPostAt).toLocaleString() : 'Never'}
Created
{new Date(bot.createdAt).toLocaleDateString()}
LLM Provider
{bot.llmProvider} / {bot.llmModel}
{/* Schedule */} {bot.autonomousMode && scheduleConfig && (

Posting Schedule

{scheduleConfig.type === 'interval' && scheduleConfig.intervalMinutes ? `Posts every ${scheduleConfig.intervalMinutes} minutes` : 'Custom schedule configured'}
)} {/* Personality */}

Personality

{personalityConfig?.systemPrompt || 'No system prompt configured'}
{/* Content Sources */}

Content Sources ({sources.length})

{showAddSource && (
{newSource.type === 'rss' && (
setNewSource({ ...newSource, url: e.target.value })} className="input" placeholder="https://example.com/feed.xml" />
)} {newSource.type === 'reddit' && (
setNewSource({ ...newSource, subreddit: e.target.value, url: `https://reddit.com/r/${e.target.value}` })} className="input" placeholder="technology" />
)} {newSource.type === 'brave_news' && ( <>
setNewSource({ ...newSource, braveQuery: e.target.value })} className="input" placeholder="AI technology, climate change, etc." />

Use quotes for exact phrases, minus to exclude terms

setNewSource({ ...newSource, apiKey: e.target.value })} className="input" placeholder="Your Brave Search API key" />

Get your API key at brave.com/search/api

)} {newSource.type === 'news_api' && ( <>
setNewSource({ ...newSource, newsQuery: e.target.value })} className="input" placeholder="technology, AI, startups" />
setNewSource({ ...newSource, apiKey: e.target.value })} className="input" placeholder="Your API key" />
)}
)} {sources.length === 0 ? (

No content sources configured. Add a source to enable posting.

) : (
{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 (
{source.type === 'brave_news' ? 'BRAVE NEWS' : source.type.toUpperCase()} - {displayName}
{source.lastFetchAt ? `Last fetched: ${new Date(source.lastFetchAt).toLocaleString()}` : 'Never fetched'} {source.lastError && • Error: {source.lastError}}
{source.isActive ? 'Active' : 'Inactive'}
); })}
)}
{/* Danger Zone */}

Danger Zone

Deleting a bot is permanent and cannot be undone. All associated data will be removed.

); }