import React, { useState, useEffect, useRef } from 'react'; const API_URL = 'http://localhost:8000'; const WS_URL = 'ws://localhost:8000'; function StorytellerView({ sessionId }) { const [characters, setCharacters] = useState({}); const [publicMessages, setPublicMessages] = useState([]); const [selectedCharacter, setSelectedCharacter] = useState(null); const [responseText, setResponseText] = useState(''); const [sceneText, setSceneText] = useState(''); const [currentScene, setCurrentScene] = useState(''); const [isConnected, setIsConnected] = useState(false); const [isGeneratingSuggestion, setIsGeneratingSuggestion] = useState(false); // Context-aware response state const [selectedCharacterIds, setSelectedCharacterIds] = useState([]); const [contextualResponseType, setContextualResponseType] = useState('scene'); const [contextualAdditionalContext, setContextualAdditionalContext] = useState(''); const [contextualModel, setContextualModel] = useState('gpt-4o'); const [isGeneratingContextual, setIsGeneratingContextual] = useState(false); const [generatedContextualResponse, setGeneratedContextualResponse] = useState(''); const [showContextualGenerator, setShowContextualGenerator] = useState(false); const wsRef = useRef(null); useEffect(() => { // Connect to WebSocket const ws = new WebSocket(`${WS_URL}/ws/storyteller/${sessionId}`); ws.onopen = () => { console.log('Storyteller connected to WebSocket'); setIsConnected(true); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'session_state') { setCharacters(data.characters || {}); setCurrentScene(data.current_scene || ''); setPublicMessages(data.public_messages || []); } else if (data.type === 'character_message') { // Update character with new message setCharacters(prev => ({ ...prev, [data.character_id]: { ...prev[data.character_id], conversation_history: [ ...(prev[data.character_id]?.conversation_history || []), data.message ], pending_response: true } })); } else if (data.type === 'character_joined') { // Refresh character list fetch(`${API_URL}/sessions/${sessionId}`) .then(res => res.json()) .then(session => { const charMap = {}; Object.entries(session.characters).forEach(([id, char]) => { charMap[id] = { ...char, conversation_history: char.conversation_history || [], pending_response: char.pending_response || false }; }); setCharacters(charMap); }); } }; ws.onclose = () => { console.log('Storyteller disconnected from WebSocket'); setIsConnected(false); }; wsRef.current = ws; return () => { ws.close(); }; }, [sessionId]); const sendResponse = () => { if (!selectedCharacter || !responseText.trim() || !isConnected) return; const message = { type: 'respond_to_character', character_id: selectedCharacter, content: responseText }; wsRef.current.send(JSON.stringify(message)); // Update local state setCharacters(prev => ({ ...prev, [selectedCharacter]: { ...prev[selectedCharacter], conversation_history: [ ...(prev[selectedCharacter]?.conversation_history || []), { sender: 'storyteller', content: responseText, timestamp: new Date().toISOString() } ], pending_response: false } })); setResponseText(''); }; const narrateScene = () => { if (!sceneText.trim() || !isConnected) return; const message = { type: 'narrate_scene', content: sceneText }; wsRef.current.send(JSON.stringify(message)); setCurrentScene(sceneText); setSceneText(''); }; const getSuggestion = async () => { if (!selectedCharacter || isGeneratingSuggestion) return; setIsGeneratingSuggestion(true); try { const response = await fetch( `${API_URL}/sessions/${sessionId}/generate_suggestion?character_id=${selectedCharacter}`, { method: 'POST' } ); if (!response.ok) { throw new Error('Failed to generate suggestion'); } const data = await response.json(); setResponseText(data.suggestion); } catch (error) { console.error('Error generating suggestion:', error); alert('Failed to generate AI suggestion. Please try again.'); } finally { setIsGeneratingSuggestion(false); } }; // Toggle character selection for contextual response const toggleCharacterSelection = (charId) => { setSelectedCharacterIds(prev => prev.includes(charId) ? prev.filter(id => id !== charId) : [...prev, charId] ); }; // Select all characters with pending messages const selectAllPending = () => { const pendingIds = Object.entries(characters) .filter(([_, char]) => char.pending_response) .map(([id, _]) => id); setSelectedCharacterIds(pendingIds); }; // Generate contextual response const generateContextualResponse = async () => { if (selectedCharacterIds.length === 0 || isGeneratingContextual) return; setIsGeneratingContextual(true); setGeneratedContextualResponse(''); try { const response = await fetch( `${API_URL}/sessions/${sessionId}/generate_contextual_response`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ character_ids: selectedCharacterIds, response_type: contextualResponseType, model: contextualModel, additional_context: contextualAdditionalContext || null }) } ); if (!response.ok) { throw new Error('Failed to generate contextual response'); } const data = await response.json(); // If individual responses were sent, show confirmation if (data.response_type === 'individual' && data.individual_responses_sent) { const sentCount = Object.keys(data.individual_responses_sent).length; const sentNames = Object.keys(data.individual_responses_sent).join(', '); if (sentCount > 0) { alert(`✅ Individual responses sent to ${sentCount} character(s): ${sentNames}\n\nThe responses have been delivered privately to each character.`); // Clear selections after successful send setSelectedCharacterIds([]); setContextualAdditionalContext(''); // Update character states to reflect no pending responses setCharacters(prev => { const updated = { ...prev }; Object.keys(data.individual_responses_sent).forEach(charName => { const charEntry = Object.entries(updated).find(([_, char]) => char.name === charName); if (charEntry) { const [charId, char] = charEntry; updated[charId] = { ...char, pending_response: false }; } }); return updated; }); } // Still show the full generated response for reference setGeneratedContextualResponse(data.response); } else { // Scene description - just show the response setGeneratedContextualResponse(data.response); } } catch (error) { console.error('Error generating contextual response:', error); alert('Failed to generate contextual response. Please try again.'); } finally { setIsGeneratingContextual(false); } }; // Use generated response as scene const useAsScene = () => { if (!generatedContextualResponse) return; setSceneText(generatedContextualResponse); setShowContextualGenerator(false); }; // Copy session ID to clipboard const copySessionId = () => { navigator.clipboard.writeText(sessionId).then(() => { alert('✅ Session ID copied to clipboard!'); }).catch(err => { console.error('Failed to copy:', err); alert('Failed to copy session ID. Please copy it manually.'); }); }; const selectedChar = selectedCharacter ? characters[selectedCharacter] : null; const pendingCount = Object.values(characters).filter(c => c.pending_response).length; return (

🎲 Storyteller Dashboard

Session ID: {sessionId}

{isConnected ? '● Connected' : '○ Disconnected'}

{pendingCount > 0 && (
{pendingCount} pending response{pendingCount !== 1 ? 's' : ''}
)}

📜 Narrate Scene to All Characters

{currentScene && (
Current Scene: {currentScene}
)}