/** * Bot Creation Page * * Form for creating a new bot. * * Requirements: 1.1, 2.1, 3.1 */ 'use client'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeftIcon } from '@/components/Icons'; import { UserStorageImageUpload } from '@/components/UserStorageImageUpload'; import { Bot, Sparkles, Rss, Clock, Trash2 } from 'lucide-react'; interface ContentSource { type: 'rss' | 'reddit' | 'news_api' | 'brave_news' | 'youtube'; url: string; subreddit?: string; apiKey?: string; // Brave News config braveQuery?: string; braveFreshness?: 'pd' | 'pw' | 'pm' | 'py'; braveCountry?: string; // News API config newsProvider?: 'newsapi' | 'gnews' | 'newsdata'; newsQuery?: string; newsCategory?: string; newsCountry?: string; newsLanguage?: string; // YouTube config youtubeChannelId?: string; youtubePlaylistId?: string; } export default function NewBotPage() { const router = useRouter(); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [step, setStep] = useState<'identity' | 'personality' | 'sources' | 'schedule'>('identity'); const [formData, setFormData] = useState({ name: '', handle: '', bio: '', avatarUrl: '', headerUrl: '', systemPrompt: '', llmProvider: 'openai', llmModel: 'gpt-4', llmApiKey: '', autonomousMode: false, postingFrequency: 'every_4_hours', customIntervalMinutes: 240, }); const [sources, setSources] = useState([]); const [newSource, setNewSource] = useState({ type: 'rss', url: '', braveQuery: '', braveFreshness: 'pw', newsProvider: 'newsapi', newsQuery: '', }); // Fetch previous bot settings to pre-fill LLM config useEffect(() => { const fetchPreviousBotSettings = async () => { try { const res = await fetch('/api/bots'); if (res.ok) { const data = await res.json(); if (data.bots && data.bots.length > 0) { // Get the most recently created bot const sortedBots = [...data.bots].sort( (a: { createdAt: string }, b: { createdAt: string }) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); const lastBot = sortedBots[0]; // Pre-fill LLM settings (but not API key for security) setFormData(prev => ({ ...prev, llmProvider: lastBot.llmProvider || 'openai', llmModel: lastBot.llmModel || 'gpt-4', })); } } } catch (err) { // Silently fail - not critical console.error('Failed to fetch previous bot settings:', err); } }; fetchPreviousBotSettings(); }, []); const handleAddSource = () => { // Validate based on type if (newSource.type === 'brave_news') { if (!newSource.braveQuery || !newSource.apiKey) return; // Build URL from config const url = new URL('https://api.search.brave.com/res/v1/news/search'); url.searchParams.set('q', newSource.braveQuery); if (newSource.braveFreshness) url.searchParams.set('freshness', newSource.braveFreshness); if (newSource.braveCountry) url.searchParams.set('country', newSource.braveCountry); setSources([...sources, { ...newSource, url: url.toString() }]); } else if (newSource.type === 'news_api') { if (!newSource.newsQuery || !newSource.apiKey) return; // Build URL from config 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); } setSources([...sources, { ...newSource, url: `${baseUrl}?${params.toString()}` }]); } else { if (!newSource.url) return; setSources([...sources, { ...newSource }]); } setNewSource({ type: 'rss', url: '', braveQuery: '', braveFreshness: 'pw', newsProvider: 'newsapi', newsQuery: '', }); }; const handleRemoveSource = (index: number) => { setSources(sources.filter((_, i) => i !== index)); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(''); try { // Calculate interval minutes based on frequency let intervalMinutes = 240; if (formData.postingFrequency === 'every_2_hours') intervalMinutes = 120; else if (formData.postingFrequency === 'every_4_hours') intervalMinutes = 240; else if (formData.postingFrequency === 'every_6_hours') intervalMinutes = 360; else if (formData.postingFrequency === 'daily') intervalMinutes = 1440; const response = await fetch('/api/bots', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: formData.name, handle: formData.handle, bio: formData.bio, avatarUrl: formData.avatarUrl || undefined, headerUrl: formData.headerUrl || undefined, personality: { systemPrompt: formData.systemPrompt, temperature: 0.7, // Default value since posts are limited to 400 chars maxTokens: 500, // Default value, sufficient for 400 char posts }, llmProvider: formData.llmProvider, llmModel: formData.llmModel, llmApiKey: formData.llmApiKey, autonomousMode: formData.autonomousMode, schedule: formData.autonomousMode ? { type: 'interval', intervalMinutes, } : undefined, }), }); if (response.ok) { const data = await response.json(); // If sources were added, create them after bot creation if (sources.length > 0) { for (const source of sources) { const sourcePayload: Record = { type: source.type, url: source.url, subreddit: source.subreddit, apiKey: source.apiKey, }; // Add config for brave_news if (source.type === 'brave_news' && source.braveQuery) { sourcePayload.braveNewsConfig = { query: source.braveQuery, freshness: source.braveFreshness, country: source.braveCountry, }; } // Add config for news_api if (source.type === 'news_api' && source.newsQuery) { sourcePayload.newsApiConfig = { provider: source.newsProvider, query: source.newsQuery, category: source.newsCategory, country: source.newsCountry, language: source.newsLanguage, }; } await fetch(`/api/bots/${data.bot.id}/sources`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sourcePayload), }); } // Trigger the first post to establish a reference point for the schedule // This runs in the background - we don't wait for it fetch(`/api/bots/${data.bot.id}/post`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }).catch(() => { // Silently fail - the bot will post on the next scheduled run }); } router.push(`/bots/${data.bot.id}`); } else { const data = await response.json(); console.error('Bot creation failed:', data); const errorMsg = data.error || 'Failed to create bot'; const detailsMsg = data.details ? '\n' + JSON.stringify(data.details, null, 2) : ''; setError(errorMsg + detailsMsg); } } catch (err) { console.error('Create bot error:', err); setError('Failed to create bot'); } finally { setLoading(false); } }; const renderStep = () => { switch (step) { case 'identity': return (
setFormData({ ...formData, name: e.target.value })} className="input" placeholder="My Awesome Bot" />
@ setFormData({ ...formData, handle: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '') })} className="input" placeholder="mybot" style={{ flex: 1 }} />

Lowercase letters, numbers, and underscores only