| import React, { useEffect, useMemo, useState } from 'react'; |
| import { Bot, CheckCircle2, PlusCircle, RefreshCw, X, ShoppingBag, Users, Globe } from 'lucide-react'; |
| import { motion } from 'framer-motion'; |
| import { supabase } from '../services/supabase'; |
| import { useAuth } from '../context/useAuth'; |
| import { getDefaultModel, getDefaultProvider, providerOptions } from '../services/llmConfig'; |
| import type { SupportedProvider } from '../services/llmConfig'; |
|
|
| interface Agent { |
| id: string; |
| name: string; |
| role: string | null; |
| api_provider: SupportedProvider; |
| model: string; |
| system_prompt: string | null; |
| created_at: string; |
| } |
|
|
| const AgentsView: React.FC = () => { |
| const { user } = useAuth(); |
| const defaultProvider = useMemo(() => getDefaultProvider(), []); |
| const [agents, setAgents] = useState<Agent[]>([]); |
| const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null); |
| const [name, setName] = useState(''); |
| const [role, setRole] = useState(''); |
| const [provider, setProvider] = useState<SupportedProvider>(defaultProvider); |
| const [model, setModel] = useState(getDefaultModel(defaultProvider)); |
| const [systemPrompt, setSystemPrompt] = useState(''); |
| const [loading, setLoading] = useState(false); |
| const [saving, setSaving] = useState(false); |
| const [message, setMessage] = useState<string | null>(null); |
| const [error, setError] = useState<string | null>(null); |
| const [showShareModal, setShowShareModal] = useState(false); |
| const [sharingAgent, setSharingAgent] = useState<Agent | null>(null); |
| const [shareToTeam, setShareToTeam] = useState<string | null>(null); |
| const [shareDescription, setShareDescription] = useState(''); |
| const [shareCategory, setShareCategory] = useState('General'); |
| const [isPublicTemplate, setIsPublicTemplate] = useState(false); |
| const [teams, setTeams] = useState<{ id: string; name: string }[]>([]); |
|
|
| const providerModels = providerOptions.find((option) => option.id === provider)?.models ?? []; |
| const isEditing = selectedAgentId !== null; |
|
|
| const loadAgents = async () => { |
| setLoading(true); |
| setError(null); |
|
|
| const { data, error: selectError } = await supabase |
| .from('agents') |
| .select('id,name,role,api_provider,model,system_prompt,created_at') |
| .order('created_at', { ascending: false }); |
|
|
| if (selectError) setError(selectError.message); |
| setAgents((data ?? []) as Agent[]); |
| setLoading(false); |
| }; |
|
|
| useEffect(() => { |
| loadAgents(); |
| fetchTeams(); |
| }, []); |
|
|
| const fetchTeams = async () => { |
| try { |
| const { data } = await supabase.from('teams').select('id, name'); |
| setTeams(data || []); |
| } catch (err) { |
| console.error('Failed to fetch teams'); |
| } |
| }; |
|
|
| const handleProviderChange = (value: SupportedProvider) => { |
| setProvider(value); |
| setModel(getDefaultModel(value)); |
| }; |
|
|
| const resetForm = () => { |
| setSelectedAgentId(null); |
| setName(''); |
| setRole(''); |
| setProvider(defaultProvider); |
| setModel(getDefaultModel(defaultProvider)); |
| setSystemPrompt(''); |
| }; |
|
|
| const selectAgent = (agent: Agent) => { |
| setSelectedAgentId(agent.id); |
| setName(agent.name); |
| setRole(agent.role ?? ''); |
| setProvider(agent.api_provider); |
| setModel(agent.model); |
| setSystemPrompt(agent.system_prompt ?? ''); |
| setMessage(null); |
| setError(null); |
| }; |
|
|
| const saveAgent = async (event: React.FormEvent) => { |
| event.preventDefault(); |
| if (!user) { |
| setError(`You must be signed in to ${isEditing ? 'update' : 'create'} an agent.`); |
| return; |
| } |
|
|
| setSaving(true); |
| setError(null); |
| setMessage(null); |
|
|
| const payload = { |
| user_id: user.id, |
| name, |
| role, |
| api_provider: provider, |
| model, |
| system_prompt: systemPrompt || `You are ${name}, acting as ${role || 'an AI agent'}.` |
| }; |
|
|
| const response = isEditing |
| ? await supabase.from('agents').update(payload).eq('id', selectedAgentId) |
| : await supabase.from('agents').insert(payload); |
|
|
| if (response.error) { |
| setError(response.error.message); |
| } else { |
| resetForm(); |
| setMessage(isEditing ? 'Agent updated successfully.' : 'Agent created successfully.'); |
| await loadAgents(); |
| } |
|
|
| setSaving(false); |
| }; |
|
|
| const handleShareTemplate = async () => { |
| if (!sharingAgent || !user) return; |
| setSaving(true); |
| try { |
| const { error } = await supabase.from('agent_templates').insert({ |
| name: sharingAgent.name, |
| role: sharingAgent.role, |
| description: shareDescription || `Custom agent: ${sharingAgent.name}`, |
| model: sharingAgent.model, |
| api_provider: sharingAgent.api_provider, |
| system_prompt: sharingAgent.system_prompt, |
| category: shareCategory, |
| author_id: user.id, |
| team_id: isPublicTemplate ? null : shareToTeam, |
| is_public: isPublicTemplate |
| }); |
|
|
| if (error) throw error; |
| setMessage('Agent shared to marketplace!'); |
| setShowShareModal(false); |
| } catch (err: any) { |
| setError(err.message); |
| } finally { |
| setSaving(false); |
| } |
| }; |
|
|
| return ( |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="agents-page animate-fade-in"> |
| <div className="dashboard-heading page-heading"> |
| <div className="panel-heading" style={{ marginBottom: 0 }}> |
| <Bot size={32} color="var(--accent)" /> |
| <div> |
| <h2>Agents</h2> |
| <p style={{ color: 'var(--text-dim)', fontSize: '0.9rem' }}>Create custom agents and choose the LLM provider used at runtime.</p> |
| </div> |
| </div> |
| <button className="btn btn-glass" onClick={loadAgents} disabled={loading}> |
| <RefreshCw size={18} /> |
| {loading ? 'Refreshing...' : 'Refresh'} |
| </button> |
| </div> |
| |
| {error && <div className="inline-status">{error}</div>} |
| {message && <div className="inline-status"><CheckCircle2 size={16} color="var(--success)" />{message}</div>} |
| |
| <div className="project-detail-grid"> |
| <section className="glass-panel project-form"> |
| <div className="settings-section-title" style={{ justifyContent: 'space-between', alignItems: 'center' }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}> |
| <PlusCircle size={22} color="var(--accent)" /> |
| <h3>{isEditing ? 'Edit Agent' : 'Create Agent'}</h3> |
| </div> |
| {isEditing && ( |
| <button className="btn btn-icon" type="button" onClick={resetForm} title="Close editor"> |
| <X size={18} /> |
| </button> |
| )} |
| </div> |
| <form onSubmit={saveAgent} style={{ display: 'grid', gap: 'var(--space-md)' }}> |
| <label> |
| <span>Name</span> |
| <input value={name} onChange={(event) => setName(event.target.value)} required placeholder="Research Analyst" /> |
| </label> |
| <label> |
| <span>Role</span> |
| <input value={role} onChange={(event) => setRole(event.target.value)} placeholder="Market research specialist" /> |
| </label> |
| <div className="responsive-two-col"> |
| <label> |
| <span>Provider</span> |
| <select value={provider} onChange={(event) => handleProviderChange(event.target.value as SupportedProvider)}> |
| {providerOptions.map((option) => ( |
| <option key={option.id} value={option.id}>{option.label}</option> |
| ))} |
| </select> |
| </label> |
| <label> |
| <span>Model</span> |
| <select value={model} onChange={(event) => setModel(event.target.value)}> |
| {providerModels.map((providerModel) => ( |
| <option key={providerModel} value={providerModel}>{providerModel}</option> |
| ))} |
| </select> |
| </label> |
| </div> |
| <label> |
| <span>System Prompt</span> |
| <textarea value={systemPrompt} onChange={(event) => setSystemPrompt(event.target.value)} rows={6} placeholder="Define behavior, boundaries, output style, and quality criteria." /> |
| </label> |
| <button className="btn btn-primary" type="submit" disabled={saving}> |
| <Bot size={18} /> |
| {saving ? (isEditing ? 'Saving...' : 'Creating...') : (isEditing ? 'Save Agent' : 'Create Agent')} |
| </button> |
| </form> |
| </section> |
| |
| <section className="glass-panel task-list-panel"> |
| <div className="settings-section-title"> |
| <Bot size={22} color="var(--accent)" /> |
| <h3>Agent Fleet</h3> |
| </div> |
| {agents.length === 0 && <p style={{ color: 'var(--text-dim)' }}>No agents created yet.</p>} |
| <div className="task-list"> |
| {agents.map((agent) => ( |
| <div |
| key={agent.id} |
| className="task-row" |
| onClick={() => selectAgent(agent)} |
| style={{ |
| width: '100%', |
| background: selectedAgentId === agent.id ? 'rgba(255,255,255,0.06)' : undefined, |
| borderColor: selectedAgentId === agent.id ? 'var(--accent)' : undefined, |
| textAlign: 'left', |
| cursor: 'pointer', |
| display: 'flex', |
| justifyContent: 'space-between', |
| alignItems: 'center' |
| }} |
| > |
| <div style={{ flex: 1, minWidth: 0 }}> |
| <strong style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{agent.name}</strong> |
| <p style={{ margin: 0, fontSize: '0.85rem', color: 'var(--text-dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{agent.role || 'No role provided.'}</p> |
| </div> |
| <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-md)', paddingLeft: 'var(--space-md)' }}> |
| <span style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--accent)', whiteSpace: 'nowrap' }}>{agent.api_provider} / {agent.model}</span> |
| <button |
| className="btn btn-glass btn-sm" |
| onClick={(e) => { |
| e.stopPropagation(); |
| setSharingAgent(agent); |
| setShowShareModal(true); |
| }} |
| title="Share to Marketplace" |
| style={{ padding: '8px' }} |
| > |
| <ShoppingBag size={14} /> |
| </button> |
| </div> |
| </div> |
| ))} |
| </div> |
| </section> |
| </div> |
| |
| {/* Share Modal */} |
| {showShareModal && sharingAgent && ( |
| <div className="modal-overlay" onClick={() => setShowShareModal(false)}> |
| <div className="glass-panel modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: '500px' }}> |
| <div className="panel-heading"> |
| <ShoppingBag size={28} color="var(--accent)" /> |
| <div> |
| <h3>Share as Template</h3> |
| <p style={{ fontSize: '0.85rem', color: 'var(--text-dim)' }}>Publish '{sharingAgent.name}' to the Marketplace.</p> |
| </div> |
| </div> |
| |
| <div style={{ display: 'grid', gap: 'var(--space-md)', marginTop: 'var(--space-lg)' }}> |
| <label> |
| <span>Description</span> |
| <textarea |
| className="glass-input" |
| value={shareDescription} |
| onChange={e => setShareDescription(e.target.value)} |
| placeholder="What is this agent expert at?" |
| rows={3} |
| /> |
| </label> |
| |
| <div className="responsive-two-col"> |
| <label> |
| <span>Category</span> |
| <select className="glass-input" value={shareCategory} onChange={e => setShareCategory(e.target.value)}> |
| <option value="General">General</option> |
| <option value="Marketing">Marketing</option> |
| <option value="Development">Development</option> |
| <option value="Legal">Legal</option> |
| <option value="Research">Research</option> |
| <option value="Finance">Finance</option> |
| </select> |
| </label> |
| <label> |
| <span>Visibility</span> |
| <select |
| className="glass-input" |
| value={isPublicTemplate ? 'public' : 'team'} |
| onChange={e => setIsPublicTemplate(e.target.value === 'public')} |
| > |
| <option value="team">Share to Team</option> |
| <option value="public">Make Public (Global)</option> |
| </select> |
| </label> |
| </div> |
| |
| {!isPublicTemplate && ( |
| <label> |
| <span>Target Team</span> |
| <select |
| className="glass-input" |
| value={shareToTeam || ''} |
| onChange={e => setShareToTeam(e.target.value || null)} |
| > |
| <option value="">Select a team...</option> |
| {teams.map(t => <option key={t.id} value={t.id}>{t.name}</option>)} |
| </select> |
| </label> |
| )} |
| |
| <div className="button-row" style={{ marginTop: 'var(--space-lg)' }}> |
| <button className="btn btn-glass" onClick={() => setShowShareModal(false)}>Cancel</button> |
| <button |
| className="btn btn-primary" |
| onClick={handleShareTemplate} |
| disabled={saving || (!isPublicTemplate && !shareToTeam)} |
| > |
| <ShoppingBag size={18} /> |
| {saving ? 'Sharing...' : 'Publish to Marketplace'} |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </motion.div> |
| ); |
| }; |
|
|
| export default AgentsView; |
|
|