← back to Norma
components/drafts/RalphyboyWidget.tsx
223 lines
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { Lightbulb, ChevronDown, ChevronRight } from 'lucide-react';
interface ChecklistState {
reviewed_news: boolean;
reviewed_x_posts: boolean;
confirmed_audience: boolean;
selected_voice: boolean;
defined_cta: boolean;
}
const CHECKLIST_ITEMS: { key: keyof ChecklistState; label: string }[] = [
{ key: 'reviewed_news', label: "Reviewed today's news" },
{ key: 'reviewed_x_posts', label: 'Reviewed X posts' },
{ key: 'confirmed_audience', label: 'Confirmed target audience' },
{ key: 'selected_voice', label: 'Selected voice mode' },
{ key: 'defined_cta', label: 'Defined key message / CTA' },
];
const DEFAULT_CHECKLIST: ChecklistState = {
reviewed_news: false,
reviewed_x_posts: false,
confirmed_audience: false,
selected_voice: false,
defined_cta: false,
};
interface RalphyboyWidgetProps {
sessionId: string;
initialChecklist?: Partial<ChecklistState>;
initialNotes?: string;
}
export default function RalphyboyWidget({
sessionId,
initialChecklist = {},
initialNotes = '',
}: RalphyboyWidgetProps) {
const [expanded, setExpanded] = useState(true);
const [checklist, setChecklist] = useState<ChecklistState>({
...DEFAULT_CHECKLIST,
...initialChecklist,
});
const [notes, setNotes] = useState(initialNotes ?? '');
const [saving, setSaving] = useState(false);
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const completedCount = Object.values(checklist).filter(Boolean).length;
const totalCount = CHECKLIST_ITEMS.length;
const persist = useCallback(
async (nextChecklist: ChecklistState, nextNotes: string) => {
setSaving(true);
try {
await fetch(`/api/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
planning_checklist: nextChecklist,
planning_notes: nextNotes,
}),
});
} catch {
/* Silent — non-critical */
} finally {
setSaving(false);
}
},
[sessionId]
);
function scheduleAutoSave(nextChecklist: ChecklistState, nextNotes: string) {
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(() => persist(nextChecklist, nextNotes), 1200);
}
/* Cleanup on unmount */
useEffect(() => {
return () => {
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
};
}, []);
function handleCheck(key: keyof ChecklistState) {
const next = { ...checklist, [key]: !checklist[key] };
setChecklist(next);
scheduleAutoSave(next, notes);
}
function handleNotes(e: React.ChangeEvent<HTMLTextAreaElement>) {
const val = e.target.value;
setNotes(val);
scheduleAutoSave(checklist, val);
}
return (
<div
className="rounded-lg overflow-hidden"
style={{
border: '1px solid var(--color-border)',
backgroundColor: 'var(--color-surface)',
}}
>
{/* Header */}
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="flex items-center justify-between w-full px-3 py-2.5"
style={{ backgroundColor: 'transparent', cursor: 'pointer' }}
aria-expanded={expanded}
aria-controls="ralphyboy-panel"
>
<div className="flex items-center gap-2">
<Lightbulb
size={14}
aria-hidden="true"
style={{ color: 'var(--color-secondary)', flexShrink: 0 }}
/>
<span className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
Planning Checklist
</span>
<span
className="text-xs px-1.5 py-0.5 rounded-full font-medium"
style={{
backgroundColor:
completedCount === totalCount
? 'rgba(34, 197, 94, 0.15)'
: 'rgba(245, 158, 11, 0.15)',
color:
completedCount === totalCount
? 'var(--color-success)'
: 'var(--color-warning)',
border: `1px solid ${
completedCount === totalCount
? 'rgba(34, 197, 94, 0.3)'
: 'rgba(245, 158, 11, 0.3)'
}`,
}}
>
{completedCount}/{totalCount}
</span>
{saving && (
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
Saving...
</span>
)}
</div>
{expanded ? (
<ChevronDown size={14} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
) : (
<ChevronRight size={14} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
)}
</button>
{/* Panel */}
{expanded && (
<div
id="ralphyboy-panel"
className="px-3 pb-3"
style={{ borderTop: '1px solid var(--color-border)' }}
>
{/* Checkboxes */}
<fieldset className="pt-2.5 pb-2">
<legend className="sr-only">Planning checklist items</legend>
<div className="flex flex-col gap-2">
{CHECKLIST_ITEMS.map(({ key, label }) => (
<label
key={key}
className="flex items-center gap-2.5 cursor-pointer group"
>
<input
type="checkbox"
checked={checklist[key]}
onChange={() => handleCheck(key)}
className="w-4 h-4 rounded"
style={{
accentColor: 'var(--color-primary)',
flexShrink: 0,
}}
/>
<span
className="text-sm transition-colors"
style={{
color: checklist[key]
? 'var(--color-text-muted)'
: 'var(--color-text-secondary)',
textDecoration: checklist[key] ? 'line-through' : 'none',
}}
>
{label}
</span>
</label>
))}
</div>
</fieldset>
{/* Notes */}
<div>
<label
htmlFor="planning-notes"
className="block text-xs font-medium mb-1"
style={{ color: 'var(--color-text-muted)' }}
>
Planning Notes
</label>
<textarea
id="planning-notes"
value={notes}
onChange={handleNotes}
placeholder="Add notes, key angles, story hooks..."
rows={3}
className="input"
style={{ resize: 'vertical', fontSize: '0.8125rem' }}
/>
</div>
</div>
)}
</div>
);
}