Add field-level inline editing for discussion guide items

Users can now edit individual questions and prompts directly without
entering section edit mode. Edit buttons appear on hover for each item,
allowing inline editing of content, time limits, and probe questions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
michael 2025-12-04 09:43:12 -06:00
parent 28c7bbc055
commit 240a1c09f5

View file

@ -342,6 +342,19 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
const [editingSubsectionId, setEditingSubsectionId] = useState<string | null>(null);
const [tempSubsectionTitle, setTempSubsectionTitle] = useState('');
// State for item-level inline editing
interface EditingItemState {
sectionId: string;
subsectionId?: string | null;
itemId: string;
itemType: 'question' | 'activity';
}
const [editingItem, setEditingItem] = useState<EditingItemState | null>(null);
const [editingItemContent, setEditingItemContent] = useState<string>('');
const [editingItemProbes, setEditingItemProbes] = useState<string[]>([]);
const [editingItemTimeLimit, setEditingItemTimeLimit] = useState<number | undefined>(undefined);
const [isSavingItem, setIsSavingItem] = useState(false);
// Drag and drop state
const [activeId, setActiveId] = useState<string | null>(null);
@ -351,12 +364,12 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
// Notify parent about editing state
// Notify parent about editing state (section or item level)
useEffect(() => {
if (onEditingChange) {
onEditingChange(!!editingSectionId);
onEditingChange(!!editingSectionId || !!editingItem);
}
}, [editingSectionId, onEditingChange]);
}, [editingSectionId, editingItem, onEditingChange]);
// Sync editing section with main guide when it changes
useEffect(() => {
@ -370,6 +383,11 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
// Functions for inline section editing
const startEditingSection = (section: DiscussionGuideSection) => {
// Prevent section editing if item is being edited
if (editingItem) {
toast.info('Please save or cancel item editing first');
return;
}
setEditingSectionId(section.id);
setEditingSection({ ...section });
// Ensure the section is open when editing starts
@ -381,6 +399,95 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
setEditingSection(null);
};
// Functions for item-level inline editing
const startEditingItem = useCallback((
sectionId: string,
itemId: string,
itemType: 'question' | 'activity',
item: DiscussionGuideItem,
subsectionId?: string | null
) => {
// Prevent item editing if section is being edited
if (editingSectionId) {
toast.info('Please save or cancel section editing first');
return;
}
setEditingItem({ sectionId, subsectionId, itemId, itemType });
setEditingItemContent(item.content);
setEditingItemProbes(item.probes || []);
setEditingItemTimeLimit(item.time_limit);
}, [editingSectionId]);
const cancelEditingItem = useCallback(() => {
setEditingItem(null);
setEditingItemContent('');
setEditingItemProbes([]);
setEditingItemTimeLimit(undefined);
}, []);
const saveEditingItem = useCallback(async () => {
if (!editingItem || !structuredGuide || !onSave) return;
setIsSavingItem(true);
try {
const updatedGuide = {
...structuredGuide,
sections: structuredGuide.sections.map(section => {
if (section.id !== editingItem.sectionId) return section;
const updateItem = (item: DiscussionGuideItem): DiscussionGuideItem => {
if (item.id !== editingItem.itemId) return item;
return {
...item,
content: editingItemContent,
probes: editingItem.itemType === 'question' ? editingItemProbes : item.probes,
time_limit: editingItemTimeLimit
};
};
if (editingItem.subsectionId) {
// Update in subsection
return {
...section,
subsections: section.subsections?.map(sub => {
if (sub.id !== editingItem.subsectionId) return sub;
return {
...sub,
questions: editingItem.itemType === 'question'
? sub.questions?.map(updateItem)
: sub.questions,
activities: editingItem.itemType === 'activity'
? sub.activities?.map(updateItem)
: sub.activities
};
})
};
}
// Update at section level
return {
...section,
questions: editingItem.itemType === 'question'
? section.questions?.map(updateItem)
: section.questions,
activities: editingItem.itemType === 'activity'
? section.activities?.map(updateItem)
: section.activities
};
})
};
await onSave(updatedGuide);
cancelEditingItem();
toast.success('Item updated successfully');
} catch (error) {
console.error('Error saving item:', error);
toast.error('Failed to save item');
} finally {
setIsSavingItem(false);
}
}, [editingItem, editingItemContent, editingItemProbes, editingItemTimeLimit, structuredGuide, onSave, cancelEditingItem]);
const updateEditingSection = useCallback((updates: Partial<DiscussionGuideSection>) => {
setEditingSection(prevSection => {
if (!prevSection) return prevSection;
@ -1040,12 +1147,136 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
);
}
// Check if this specific item is being edited (item-level editing)
const subsectionId = subsectionIndex !== undefined ? section?.subsections?.[subsectionIndex]?.id : null;
const isItemEditing = editingItem?.itemId === item.id &&
editingItem?.sectionId === section?.id &&
editingItem?.subsectionId === subsectionId;
// Render item-level inline edit mode
if (isItemEditing) {
return (
<div
key={item.id}
className="flex items-start gap-3 p-3 rounded-lg border bg-white border-blue-300 ring-2 ring-blue-200"
>
<div className="flex-shrink-0 mt-1">
<Edit3 className="h-4 w-4 text-blue-600" />
</div>
<div className="flex-1 min-w-0 space-y-3">
{/* Badge row with time limit editor */}
<div className="flex items-center gap-2 mb-2 flex-wrap">
<Badge variant="outline" className="text-xs whitespace-nowrap">
{itemType === 'activity' ? (
<>
<Activity className="h-3 w-3 mr-1" />
{typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')}
</>
) : (
<>
<MessageCircle className="h-3 w-3 mr-1" />
{typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')}
</>
)}
</Badge>
<div className="flex items-center gap-1 text-xs text-slate-500">
<Clock className="h-3 w-3" />
<Input
type="number"
value={editingItemTimeLimit || ''}
onChange={(e) => setEditingItemTimeLimit(parseInt(e.target.value) || undefined)}
className="w-16 h-6 text-xs"
placeholder="min"
min={1}
/>
<span>min</span>
</div>
</div>
{/* Content textarea */}
<Textarea
value={editingItemContent}
onChange={(e) => setEditingItemContent(e.target.value)}
placeholder="Enter content..."
className="min-h-[60px]"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Escape') {
cancelEditingItem();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
saveEditingItem();
}
}}
/>
{/* Probes for questions */}
{itemType === 'question' && (
<div>
<label className="text-xs font-medium text-slate-700 mb-1 block">
Probe Questions (one per line)
</label>
<Textarea
value={editingItemProbes.join('\n')}
onChange={(e) => {
const probes = e.target.value.trim()
? e.target.value.split('\n').filter(p => p.trim())
: [];
setEditingItemProbes(probes);
}}
placeholder="Enter probe questions, one per line..."
className="min-h-[40px] text-sm"
onKeyDown={(e) => {
if (e.key === 'Escape') {
cancelEditingItem();
}
}}
/>
</div>
)}
{/* Save/Cancel buttons */}
<div className="flex items-center gap-2 pt-2 border-t">
<Button
size="sm"
variant="default"
onClick={saveEditingItem}
disabled={isSavingItem}
>
{isSavingItem ? (
<Loader2 className="h-3 w-3 animate-spin mr-1" />
) : (
<Save className="h-3 w-3 mr-1" />
)}
Save
</Button>
<Button
size="sm"
variant="ghost"
onClick={cancelEditingItem}
disabled={isSavingItem}
>
<X className="h-3 w-3 mr-1" />
Cancel
</Button>
<span className="text-xs text-slate-400 ml-auto">
Esc to cancel, {navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}+Enter to save
</span>
</div>
</div>
</div>
);
}
// Render read-only version
return (
<div
key={item.id}
<div
key={item.id}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border transition-colors",
"group flex items-start gap-3 p-3 rounded-lg border transition-colors",
isCurrent && "bg-blue-50 border-blue-200",
isCompleted && "bg-green-50 border-green-200",
!isCurrent && !isCompleted && "bg-slate-50 border-slate-200",
@ -1102,8 +1333,32 @@ const DiscussionGuideViewer: React.FC<DiscussionGuideViewerProps> = React.memo((
Set Position
</Button>
)}
{onSave && (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
startEditingItem(
structuredGuide!.sections[sectionIndex].id,
item.id,
itemType,
item,
subsectionId
);
}}
className={cn(
"h-6 px-2 opacity-0 group-hover:opacity-100 transition-opacity",
!onSetPosition && "ml-auto"
)}
>
<Edit3 className="h-3 w-3 mr-1" />
Edit
</Button>
)}
</div>
<p className="text-sm text-slate-700 whitespace-pre-wrap">{item.content}</p>
{item.probes && item.probes.length > 0 && (