import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { ChevronRight, ChevronDown, Clock, MessageCircle, Activity, CheckCircle, Circle, PlayCircle, Target, Edit3, Trash2, Save, X, Download, Loader2, Image as ImageIcon, ChevronUp, Check } from 'lucide-react'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; import { focusGroupsApi } from '@/lib/api'; interface DiscussionGuideItem { id: string; content: string; type: string; time_limit?: number; probes?: string[]; metadata?: Record; } interface DiscussionGuideSubsection { id: string; title: string; questions: DiscussionGuideItem[]; activities?: DiscussionGuideItem[]; metadata?: Record; } interface DiscussionGuideSection { id: string; title: string; content?: string; questions?: DiscussionGuideItem[]; activities?: DiscussionGuideItem[]; subsections?: DiscussionGuideSubsection[]; metadata?: Record; } interface StructuredDiscussionGuide { title: string; total_duration: number; sections: DiscussionGuideSection[]; metadata?: Record; } interface ModeratorStatus { current_section: string; current_section_id: string; current_item: string; current_item_id: string; current_item_type: string; progress: number; section_progress: number; total_sections: number; moderator_position: { section_index: number; subsection_index?: number; item_index: number; item_type: string; }; section_type: string; legacy_format: boolean; } interface DiscussionGuideViewerProps { discussionGuide: StructuredDiscussionGuide | string; moderatorStatus?: ModeratorStatus; onSectionSelect?: (sectionId: string, itemId?: string) => void; onSetPosition?: (sectionId: string, itemId: string, content: string, sectionTitle: string, itemTitle?: string, itemType?: string, metadata?: Record) => void; onSave?: (updatedGuide: StructuredDiscussionGuide) => Promise; showProgress?: boolean; collapsible?: boolean; defaultExpanded?: boolean; className?: string; onDownload?: () => void; isDownloading?: boolean; focusGroupId?: string; onEditingChange?: (isEditing: boolean) => void; } const DiscussionGuideViewer: React.FC = React.memo((props) => { const { discussionGuide, moderatorStatus, onSectionSelect, onSetPosition, onSave, showProgress = true, collapsible = true, defaultExpanded = false, className, onDownload, isDownloading = false, focusGroupId, onEditingChange } = props; // Handle legacy markdown format const isLegacyFormat = typeof discussionGuide === 'string'; // Parse structured guide const structuredGuide = useMemo(() => { if (isLegacyFormat) { return null; } const guide = discussionGuide as StructuredDiscussionGuide; return guide; }, [discussionGuide, isLegacyFormat]); // Initialize openSections state const [openSections, setOpenSections] = useState>(new Set()); // State for inline section editing const [editingSectionId, setEditingSectionId] = useState(null); const [editingSection, setEditingSection] = useState(null); const [isSaving, setIsSaving] = useState(false); const [editingSubsectionId, setEditingSubsectionId] = useState(null); const [tempSubsectionTitle, setTempSubsectionTitle] = useState(''); // Notify parent about editing state useEffect(() => { if (onEditingChange) { onEditingChange(!!editingSectionId); } }, [editingSectionId, onEditingChange]); // Sync editing section with main guide when it changes useEffect(() => { if (editingSectionId && structuredGuide) { const currentSection = structuredGuide.sections.find(s => s.id === editingSectionId); if (currentSection && !editingSection) { setEditingSection({ ...currentSection }); } } }, [structuredGuide, editingSectionId, editingSection]); // Functions for inline section editing const startEditingSection = (section: DiscussionGuideSection) => { setEditingSectionId(section.id); setEditingSection({ ...section }); // Ensure the section is open when editing starts setOpenSections(prev => new Set(prev).add(section.id)); }; const cancelEditingSection = () => { setEditingSectionId(null); setEditingSection(null); }; const updateEditingSection = useCallback((updates: Partial) => { setEditingSection(prevSection => { if (!prevSection) return prevSection; return { ...prevSection, ...updates }; }); }, []); const updateEditingSectionItem = useCallback((itemId: string, updates: Partial, itemType: 'question' | 'activity') => { setEditingSection(prevSection => { if (!prevSection) return prevSection; const newSection = { ...prevSection }; // First try to update in section-level items if (itemType === 'question' && newSection.questions) { const questionIndex = newSection.questions.findIndex(q => q.id === itemId); if (questionIndex !== -1) { newSection.questions = newSection.questions.map(q => q.id === itemId ? { ...q, ...updates } : q ); return newSection; } } else if (itemType === 'activity' && newSection.activities) { const activityIndex = newSection.activities.findIndex(a => a.id === itemId); if (activityIndex !== -1) { newSection.activities = newSection.activities.map(a => a.id === itemId ? { ...a, ...updates } : a ); return newSection; } } // If not found in section-level, search in subsections if (newSection.subsections) { newSection.subsections = newSection.subsections.map(subsection => { const updatedSubsection = { ...subsection }; if (itemType === 'question' && updatedSubsection.questions) { const questionIndex = updatedSubsection.questions.findIndex(q => q.id === itemId); if (questionIndex !== -1) { updatedSubsection.questions = updatedSubsection.questions.map(q => q.id === itemId ? { ...q, ...updates } : q ); } } else if (itemType === 'activity' && updatedSubsection.activities) { const activityIndex = updatedSubsection.activities.findIndex(a => a.id === itemId); if (activityIndex !== -1) { updatedSubsection.activities = updatedSubsection.activities.map(a => a.id === itemId ? { ...a, ...updates } : a ); } } return updatedSubsection; }); } return newSection; }); }, []); const addEditingSectionItem = (itemType: 'question' | 'activity') => { if (!editingSection) return; const newItem: DiscussionGuideItem = { id: `${itemType}-${Date.now()}`, content: `New ${itemType}`, type: itemType === 'question' ? 'open_ended' : 'discussion', time_limit: undefined }; const updatedSection = { ...editingSection }; if (itemType === 'question') { updatedSection.questions = [...(updatedSection.questions || []), newItem]; } else { updatedSection.activities = [...(updatedSection.activities || []), newItem]; } setEditingSection(updatedSection); }; const addSubsectionItem = (subsectionIndex: number, itemType: 'question' | 'activity') => { if (!editingSection || !editingSection.subsections) return; const newItem: DiscussionGuideItem = { id: `${itemType}-${Date.now()}`, content: `New ${itemType}`, type: itemType === 'question' ? 'open_ended' : 'discussion', time_limit: undefined }; const updatedSubsections = [...editingSection.subsections]; const targetSubsection = { ...updatedSubsections[subsectionIndex] }; if (itemType === 'question') { targetSubsection.questions = [...(targetSubsection.questions || []), newItem]; } else { targetSubsection.activities = [...(targetSubsection.activities || []), newItem]; } updatedSubsections[subsectionIndex] = targetSubsection; setEditingSection(prev => prev ? { ...prev, subsections: updatedSubsections } : prev); }; const addSubsection = () => { if (!editingSection) return; const newSubsection = { id: `subsection-${Date.now()}`, title: 'New Subsection', questions: [], activities: [] }; const updatedSubsections = [...(editingSection.subsections || []), newSubsection]; setEditingSection(prev => prev ? { ...prev, subsections: updatedSubsections } : prev); }; const deleteSubsection = (subsectionIndex: number) => { if (!editingSection || !editingSection.subsections) return; const updatedSubsections = editingSection.subsections.filter((_, index) => index !== subsectionIndex); setEditingSection(prev => prev ? { ...prev, subsections: updatedSubsections } : prev); }; const deleteEditingSectionItem = (itemId: string, itemType: 'question' | 'activity') => { if (!editingSection) return; const updatedSection = { ...editingSection }; if (itemType === 'question') { updatedSection.questions = updatedSection.questions?.filter(q => q.id !== itemId); } else { updatedSection.activities = updatedSection.activities?.filter(a => a.id !== itemId); } setEditingSection(updatedSection); }; const saveEditingSection = async () => { if (!editingSection || !structuredGuide || !onSave) return; setIsSaving(true); try { const updatedGuide = { ...structuredGuide, sections: structuredGuide.sections.map(s => s.id === editingSectionId ? editingSection : s ) }; await onSave(updatedGuide); cancelEditingSection(); toast.success('Section updated successfully'); } catch (error) { console.error('Error saving section:', error); toast.error('Failed to save section'); } finally { setIsSaving(false); } }; // Toggle section expand/collapse const toggleSection = (sectionId: string) => { setOpenSections(prev => { const newSet = new Set(prev); if (newSet.has(sectionId)) { newSet.delete(sectionId); } else { newSet.add(sectionId); } return newSet; }); }; // Effect to manage default expansion state useEffect(() => { if (structuredGuide && structuredGuide.sections.length > 0) { if (defaultExpanded) { setOpenSections(new Set(structuredGuide.sections.map(s => s.id))); } else { setOpenSections(new Set()); } } }, [defaultExpanded, structuredGuide]); // Get item status based on moderator position const getItemStatus = ( sectionIndex: number, itemIndex: number, itemType: string, subsectionIndex?: number ) => { if (!moderatorStatus || moderatorStatus.legacy_format) return null; const pos = moderatorStatus.moderator_position; // Check if we're in the right section if (pos.section_index !== sectionIndex) { return pos.section_index > sectionIndex ? 'completed' : null; } // Check subsection if applicable if (subsectionIndex !== undefined) { if (pos.subsection_index === undefined) return null; if (pos.subsection_index !== subsectionIndex) { return pos.subsection_index > subsectionIndex ? 'completed' : null; } } else if (pos.subsection_index !== undefined) { // We're in main section but moderator is in a subsection return 'completed'; } // Check item type if (pos.item_type !== itemType) { // Activities typically come before questions in our structure if (itemType === 'activity' && pos.item_type === 'question') { return 'completed'; } return null; } // Check item index if (pos.item_index === itemIndex) { return 'current'; } return pos.item_index > itemIndex ? 'completed' : null; }; // Helper function to check if content is default placeholder text const isDefaultPlaceholderContent = (content: string, itemType: 'question' | 'activity'): boolean => { return content === `New ${itemType}`; }; // Helper functions for array reordering const moveArrayItem = useCallback((array: any[], fromIndex: number, toIndex: number) => { if (fromIndex < 0 || fromIndex >= array.length || toIndex < 0 || toIndex >= array.length) { return array; } const newArray = [...array]; const [movedItem] = newArray.splice(fromIndex, 1); newArray.splice(toIndex, 0, movedItem); return newArray; }, []); const canMoveItemUp = useCallback((items: any[], index: number) => { return index > 0; }, []); const canMoveItemDown = useCallback((items: any[], index: number) => { return index < items.length - 1; }, []); // Move subsection up within section const moveSubsectionUp = useCallback((subsectionIndex: number) => { if (!editingSection || !editingSection.subsections) return; const subsections = editingSection.subsections; if (canMoveItemUp(subsections, subsectionIndex)) { const newSubsections = moveArrayItem(subsections, subsectionIndex, subsectionIndex - 1); setEditingSection(prev => prev ? { ...prev, subsections: newSubsections } : prev); } }, [editingSection, canMoveItemUp, moveArrayItem]); // Move subsection down within section const moveSubsectionDown = useCallback((subsectionIndex: number) => { if (!editingSection || !editingSection.subsections) return; const subsections = editingSection.subsections; if (canMoveItemDown(subsections, subsectionIndex)) { const newSubsections = moveArrayItem(subsections, subsectionIndex, subsectionIndex + 1); setEditingSection(prev => prev ? { ...prev, subsections: newSubsections } : prev); } }, [editingSection, canMoveItemDown, moveArrayItem]); // Subsection title editing functions const startEditingSubsectionTitle = useCallback((subsectionId: string, currentTitle: string) => { setEditingSubsectionId(subsectionId); setTempSubsectionTitle(currentTitle); }, []); const cancelEditingSubsectionTitle = useCallback(() => { setEditingSubsectionId(null); setTempSubsectionTitle(''); }, []); const saveSubsectionTitle = useCallback(() => { if (!editingSubsectionId || !editingSection || !editingSection.subsections) return; const updatedSubsections = editingSection.subsections.map(subsection => subsection.id === editingSubsectionId ? { ...subsection, title: tempSubsectionTitle.trim() } : subsection ); setEditingSection(prev => prev ? { ...prev, subsections: updatedSubsections } : prev); cancelEditingSubsectionTitle(); }, [editingSubsectionId, editingSection, tempSubsectionTitle, cancelEditingSubsectionTitle]); // Move item up within section or subsection const moveEditingItemUp = useCallback((itemId: string, itemType: 'question' | 'activity', itemIndex: number, subsectionIndex?: number) => { if (!editingSection) return; const itemsKey = itemType === 'question' ? 'questions' : 'activities'; if (subsectionIndex !== undefined) { // Handle subsection items const subsections = editingSection.subsections || []; if (subsectionIndex >= 0 && subsectionIndex < subsections.length) { const subsection = subsections[subsectionIndex]; const items = subsection[itemsKey] || []; if (canMoveItemUp(items, itemIndex)) { const newItems = moveArrayItem(items, itemIndex, itemIndex - 1); const newSubsections = [...subsections]; newSubsections[subsectionIndex] = { ...subsection, [itemsKey]: newItems }; setEditingSection(prev => prev ? { ...prev, subsections: newSubsections } : prev); } } } else { // Handle section-level items const items = editingSection[itemsKey] || []; if (canMoveItemUp(items, itemIndex)) { const newItems = moveArrayItem(items, itemIndex, itemIndex - 1); setEditingSection(prev => prev ? { ...prev, [itemsKey]: newItems } : prev); } } }, [editingSection, canMoveItemUp, moveArrayItem]); // Move item down within section or subsection const moveEditingItemDown = useCallback((itemId: string, itemType: 'question' | 'activity', itemIndex: number, subsectionIndex?: number) => { if (!editingSection) return; const itemsKey = itemType === 'question' ? 'questions' : 'activities'; if (subsectionIndex !== undefined) { // Handle subsection items const subsections = editingSection.subsections || []; if (subsectionIndex >= 0 && subsectionIndex < subsections.length) { const subsection = subsections[subsectionIndex]; const items = subsection[itemsKey] || []; if (canMoveItemDown(items, itemIndex)) { const newItems = moveArrayItem(items, itemIndex, itemIndex + 1); const newSubsections = [...subsections]; newSubsections[subsectionIndex] = { ...subsection, [itemsKey]: newItems }; setEditingSection(prev => prev ? { ...prev, subsections: newSubsections } : prev); } } } else { // Handle section-level items const items = editingSection[itemsKey] || []; if (canMoveItemDown(items, itemIndex)) { const newItems = moveArrayItem(items, itemIndex, itemIndex + 1); setEditingSection(prev => prev ? { ...prev, [itemsKey]: newItems } : prev); } } }, [editingSection, canMoveItemDown, moveArrayItem]); // Render an item (activity or question) with support for editing const renderItem = ( item: DiscussionGuideItem, sectionIndex: number, itemIndex: number, itemType: 'activity' | 'question', subsectionIndex?: number ) => { const section = structuredGuide?.sections[sectionIndex]; const isEditing = editingSectionId === section?.id; const status = getItemStatus(sectionIndex, itemIndex, itemType, subsectionIndex); const isCurrent = status === 'current'; const isCompleted = status === 'completed'; // Get visual asset filename from metadata instead of extracting from content const getVisualAssetFilename = (item: DiscussionGuideItem): string | null => { return item.metadata?.visual_asset?.filename || null; }; const imageFilename = getVisualAssetFilename(item); // Check if this is a default placeholder item const isPlaceholder = isDefaultPlaceholderContent(item.content, itemType); if (isEditing) { // Render editable version return (
{itemType === 'activity' ? ( <> {typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')} ) : ( <> {typeof item.type === 'string' ? item.type.replace('_', ' ') : String(item.type || 'unknown')} )} {item.time_limit && (
updateEditingSectionItem(item.id, { time_limit: parseInt(e.target.value) || undefined }, itemType)} className="w-16 h-6 text-xs" placeholder="min" /> min
)}