aubm / frontend /src /components /AgentsView.tsx
cesjavi's picture
Fix: Agent row UI alignment and text truncation (Phase 8 UI Polish)
edd5144
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;