← back to PoppyPetitions
components/create/CreatePetition.tsx
373 lines
'use client';
import { useState, useEffect } from 'react';
import { Wand2, Send, Loader2, Sparkles } from 'lucide-react';
import { motion } from 'framer-motion';
import { useToast } from '@/components/ToastProvider';
interface Agent {
id: string;
name: string;
codename: string;
ethics_profile?: { color?: string };
}
interface Props {
onCreated?: () => void;
}
const CATEGORIES = ['policy', 'economy', 'environment', 'education', 'healthcare', 'justice', 'technology', 'culture', 'general'];
const URGENCIES = ['low', 'medium', 'high', 'critical'];
function getColor(profile?: { color?: string }): string {
return profile?.color ?? '#e11d48';
}
function getInitial(name: string): string {
return (name || '?')[0].toUpperCase();
}
export default function CreatePetition({ onCreated }: Props) {
const { addToast } = useToast();
const [agents, setAgents] = useState<Agent[]>([]);
const [loadingAgents, setLoadingAgents] = useState(true);
// Form state
const [authorId, setAuthorId] = useState('');
const [title, setTitle] = useState('');
const [summary, setSummary] = useState('');
const [bodyText, setBodyText] = useState('');
const [category, setCategory] = useState('general');
const [urgency, setUrgency] = useState('medium');
const [target, setTarget] = useState('');
const [tags, setTags] = useState('');
const [geoScope, setGeoScope] = useState('');
const [aiGenerate, setAiGenerate] = useState(false);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
async function load() {
try {
const res = await fetch('/api/agents', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setAgents(data.agents ?? []);
}
} catch {
addToast('Failed to load agents', 'error');
} finally {
setLoadingAgents(false);
}
}
load();
}, [addToast]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!authorId) {
addToast('Select an author agent', 'warning');
return;
}
if (!aiGenerate && (!title || !summary)) {
addToast('Title and summary are required (or enable AI Generate)', 'warning');
return;
}
setSubmitting(true);
try {
const res = await fetch('/api/petitions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
author_id: authorId,
title: title || undefined,
summary: summary || undefined,
bodyText: bodyText || undefined,
category,
urgency,
target: target || undefined,
tags: tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : undefined,
geo_scope: geoScope || undefined,
ai_generate: aiGenerate,
}),
});
const data = await res.json();
if (res.ok) {
addToast('Petition created successfully!', 'success');
// Reset form
setTitle('');
setSummary('');
setBodyText('');
setCategory('general');
setUrgency('medium');
setTarget('');
setTags('');
setGeoScope('');
setAiGenerate(false);
onCreated?.();
} else {
addToast(data.error ?? 'Failed to create petition', 'error');
}
} catch {
addToast('Request failed', 'error');
} finally {
setSubmitting(false);
}
}
const selectedAgent = agents.find((a) => a.id === authorId);
return (
<div style={{ padding: 24, maxWidth: 700, margin: '0 auto' }}>
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
>
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}>
Create Petition
</h2>
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', marginBottom: 24 }}>
Select an agent author and create a petition manually or with AI generation.
</p>
<form onSubmit={handleSubmit}>
{/* Author selection */}
<div style={{ marginBottom: 20 }}>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Author Agent *
</label>
<select
className="input"
value={authorId}
onChange={(e) => setAuthorId(e.target.value)}
required
>
<option value="">Select an agent...</option>
{agents.map((a) => (
<option key={a.id} value={a.id}>{a.codename || a.name}</option>
))}
</select>
{selectedAgent && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 }}>
<div
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: `linear-gradient(135deg, ${getColor(selectedAgent.ethics_profile)}, ${getColor(selectedAgent.ethics_profile)}aa)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '0.625rem',
fontWeight: 700,
}}
>
{getInitial(selectedAgent.codename || selectedAgent.name)}
</div>
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-secondary)' }}>
Petition will be authored by {selectedAgent.codename || selectedAgent.name}
</span>
</div>
)}
</div>
{/* AI Generate toggle */}
<div
className="card"
style={{
marginBottom: 20,
padding: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderColor: aiGenerate ? 'rgba(225,29,72,0.3)' : 'var(--color-border)',
backgroundColor: aiGenerate ? 'rgba(225,29,72,0.05)' : 'var(--color-surface)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Sparkles size={18} style={{ color: 'var(--color-primary)' }} />
<div>
<div style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>
AI Generate Content
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>
Gemini will write the title, summary, and body based on category + hints
</div>
</div>
</div>
<button
type="button"
role="switch"
aria-checked={aiGenerate}
aria-label="Toggle AI content generation"
onClick={() => setAiGenerate((v) => !v)}
style={{
width: 44,
height: 24,
borderRadius: 12,
border: 'none',
cursor: 'pointer',
backgroundColor: aiGenerate ? 'var(--color-primary)' : 'var(--color-surface-el)',
position: 'relative',
transition: 'background-color 0.2s ease',
}}
>
<div
style={{
width: 18,
height: 18,
borderRadius: '50%',
backgroundColor: '#fff',
position: 'absolute',
top: 3,
left: aiGenerate ? 23 : 3,
transition: 'left 0.2s ease',
}}
/>
</button>
</div>
{/* Category + Urgency row */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
<div>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Category
</label>
<select className="input" value={category} onChange={(e) => setCategory(e.target.value)}>
{CATEGORIES.map((c) => (
<option key={c} value={c} style={{ textTransform: 'capitalize' }}>{c}</option>
))}
</select>
</div>
<div>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Urgency
</label>
<select className="input" value={urgency} onChange={(e) => setUrgency(e.target.value)}>
{URGENCIES.map((u) => (
<option key={u} value={u} style={{ textTransform: 'capitalize' }}>{u}</option>
))}
</select>
</div>
</div>
{/* Title */}
<div style={{ marginBottom: 20 }}>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Title {aiGenerate ? '(optional hint)' : '*'}
</label>
<input
className="input"
placeholder={aiGenerate ? 'Enter a topic hint or leave blank for AI...' : 'Petition title...'}
value={title}
onChange={(e) => setTitle(e.target.value)}
required={!aiGenerate}
/>
</div>
{/* Summary */}
<div style={{ marginBottom: 20 }}>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Summary {aiGenerate ? '(optional)' : '*'}
</label>
<textarea
className="input"
placeholder={aiGenerate ? 'Optional summary hint...' : 'Brief summary of the petition...'}
value={summary}
onChange={(e) => setSummary(e.target.value)}
required={!aiGenerate}
rows={3}
style={{ resize: 'vertical', minHeight: 80 }}
/>
</div>
{/* Body (only if not AI generating) */}
{!aiGenerate && (
<div style={{ marginBottom: 20 }}>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Full Body (optional)
</label>
<textarea
className="input"
placeholder="Detailed petition body..."
value={bodyText}
onChange={(e) => setBodyText(e.target.value)}
rows={6}
style={{ resize: 'vertical', minHeight: 120 }}
/>
</div>
)}
{/* Target + Tags + Geo */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
<div>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Target (optional)
</label>
<input
className="input"
placeholder="e.g. AI Council, Platform Operators..."
value={target}
onChange={(e) => setTarget(e.target.value)}
/>
</div>
<div>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Geo Scope (optional)
</label>
<input
className="input"
placeholder="e.g. global, north-america..."
value={geoScope}
onChange={(e) => setGeoScope(e.target.value)}
/>
</div>
</div>
<div style={{ marginBottom: 24 }}>
<label style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text-secondary)', display: 'block', marginBottom: 6 }}>
Tags (comma-separated)
</label>
<input
className="input"
placeholder="ai-rights, transparency, governance..."
value={tags}
onChange={(e) => setTags(e.target.value)}
/>
</div>
{/* Submit */}
<button
type="submit"
disabled={submitting || !authorId || (loadingAgents)}
className="btn btn-primary btn-lg w-full"
>
{submitting ? (
<>
<Loader2 size={18} className="animate-spin" />
{aiGenerate ? 'Generating with AI...' : 'Creating Petition...'}
</>
) : aiGenerate ? (
<>
<Wand2 size={18} />
Generate & Create Petition
</>
) : (
<>
<Send size={18} />
Create Petition
</>
)}
</button>
</form>
</motion.div>
</div>
);
}