Add context-aware response generator, demo session, and bug fixes
Features: - Context-aware response generator for storyteller - Select multiple characters to include in context - Generate scene descriptions or individual responses - Individual responses auto-parsed and sent to each character - Improved prompt with explicit [CharacterName] format - Smart context building with character profiles and history - Demo session auto-creation on startup - Pre-configured 'The Cursed Tavern' adventure - Two characters: Bargin (Dwarf Warrior) and Willow (Elf Ranger) - Quick-access buttons on home page - Eliminates need to recreate test data - Session ID copy button for easy sharing Bug Fixes: - Fixed character chat history showing only most recent message - CharacterView now handles both 'storyteller_response' and 'new_message' - Fixed all Pydantic deprecation warnings - Replaced .dict() with .model_dump() (9 instances) - Fixed WebSocket manager reference in contextual responses UI Improvements: - Beautiful demo section with gradient styling - Format help text for individual responses - Improved messaging and confirmations Documentation: - CONTEXTUAL_RESPONSE_FEATURE.md - Complete feature documentation - DEMO_SESSION.md - Demo session guide - FIXES_SUMMARY.md - Bug fix summary - PROMPT_IMPROVEMENTS.md - Prompt engineering details
This commit is contained in:
@@ -45,6 +45,66 @@ body {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Demo Session Section */
|
||||
.demo-section {
|
||||
background: linear-gradient(135deg, #ffd89b 0%, #19547b 100%);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 2rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
opacity: 0.95;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.demo-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-demo {
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-demo:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-demo:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-storyteller {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-character {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.setup-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@@ -388,16 +448,49 @@ body {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.session-id-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.session-id {
|
||||
opacity: 0.9;
|
||||
font-size: 0.9rem;
|
||||
color: #718096;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.session-id code {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #edf2f7;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #2d3748;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
border: 2px solid #48bb78;
|
||||
background: white;
|
||||
color: #48bb78;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-copy:hover {
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(72, 187, 120, 0.2);
|
||||
}
|
||||
|
||||
.btn-copy:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.pending-badge {
|
||||
@@ -872,6 +965,264 @@ body {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Contextual Response Generator */
|
||||
.contextual-section {
|
||||
margin: 1.5rem 1rem;
|
||||
background: #fff5f5;
|
||||
border: 2px solid #fc8181;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contextual-header {
|
||||
background: linear-gradient(135deg, #fc8181 0%, #f56565 100%);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contextual-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.contextual-generator {
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.contextual-description {
|
||||
color: #4a5568;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Character Selection */
|
||||
.character-selection {
|
||||
background: #f7fafc;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.selection-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.selection-header h4 {
|
||||
margin: 0;
|
||||
color: #2d3748;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
border: 2px solid #667eea;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-small:hover:not(:disabled) {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-small:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.character-checkboxes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.character-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.character-checkbox:hover {
|
||||
border-color: #667eea;
|
||||
background: #f0f4ff;
|
||||
}
|
||||
|
||||
.character-checkbox.has-pending {
|
||||
border-color: #fc8181;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.character-checkbox input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.pending-badge-small {
|
||||
color: #fc8181;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.message-count {
|
||||
color: #a0aec0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.selection-summary {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #edf2f7;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
/* Response Type and Model Selectors */
|
||||
.response-type-selector,
|
||||
.model-selector-contextual {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.response-type-selector label,
|
||||
.model-selector-contextual label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.response-type-selector select,
|
||||
.model-selector-contextual select {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.response-type-selector select:focus,
|
||||
.model-selector-contextual select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.response-type-help {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: #ebf8ff;
|
||||
border-left: 3px solid #4299e1;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: #2c5282;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.response-type-help code {
|
||||
background: #2c5282;
|
||||
color: #ebf8ff;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Additional Context */
|
||||
.additional-context {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.additional-context label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.additional-context textarea {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.additional-context textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* Generate Button */
|
||||
.btn-large {
|
||||
width: 100%;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Generated Response Display */
|
||||
.generated-response {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: #f0fdf4;
|
||||
border: 2px solid #86efac;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.generated-response h4 {
|
||||
color: #166534;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.response-content {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
color: #2d3748;
|
||||
font-size: 1rem;
|
||||
border-left: 4px solid #86efac;
|
||||
}
|
||||
|
||||
.response-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.response-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.storyteller-content {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -40,7 +40,7 @@ function CharacterView({ sessionId, characterId }) {
|
||||
if (data.type === 'history') {
|
||||
setMessages(data.messages || []);
|
||||
setPublicMessages(data.public_messages || []);
|
||||
} else if (data.type === 'storyteller_response') {
|
||||
} else if (data.type === 'storyteller_response' || data.type === 'new_message') {
|
||||
setMessages(prev => [...prev, data.message]);
|
||||
} else if (data.type === 'scene_narration') {
|
||||
setCurrentScene(data.content);
|
||||
|
||||
@@ -78,12 +78,48 @@ function SessionSetup({ onCreateSession, onJoinSession }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Quick join demo session functions
|
||||
const joinDemoStoryteller = () => {
|
||||
onCreateSession("demo-session-001");
|
||||
};
|
||||
|
||||
const joinDemoBargin = () => {
|
||||
onJoinSession("demo-session-001", "char-bargin-001");
|
||||
};
|
||||
|
||||
const joinDemoWillow = () => {
|
||||
onJoinSession("demo-session-001", "char-willow-002");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="session-setup">
|
||||
<div className="setup-container">
|
||||
<h1>🎭 Storyteller RPG</h1>
|
||||
<p className="subtitle">Private character-storyteller interactions</p>
|
||||
|
||||
{/* Demo Session Quick Access */}
|
||||
<div className="demo-section">
|
||||
<h2>🎲 Demo Session - "The Cursed Tavern"</h2>
|
||||
<p className="demo-description">
|
||||
Jump right into a pre-configured adventure with two characters already created!
|
||||
</p>
|
||||
<div className="demo-buttons">
|
||||
<button className="btn-demo btn-storyteller" onClick={joinDemoStoryteller}>
|
||||
🎲 Join as Storyteller
|
||||
</button>
|
||||
<button className="btn-demo btn-character" onClick={joinDemoBargin}>
|
||||
⚔️ Play as Bargin (Dwarf Warrior)
|
||||
</button>
|
||||
<button className="btn-demo btn-character" onClick={joinDemoWillow}>
|
||||
🏹 Play as Willow (Elf Ranger)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider">
|
||||
<span>OR CREATE YOUR OWN</span>
|
||||
</div>
|
||||
|
||||
<div className="setup-section">
|
||||
<h2>Create New Session</h2>
|
||||
<p className="section-description">Start a new game as the storyteller</p>
|
||||
|
||||
@@ -12,6 +12,16 @@ function StorytellerView({ sessionId }) {
|
||||
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(() => {
|
||||
@@ -137,6 +147,108 @@ function StorytellerView({ sessionId }) {
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -145,7 +257,14 @@ function StorytellerView({ sessionId }) {
|
||||
<div className="storyteller-header">
|
||||
<div>
|
||||
<h1>🎲 Storyteller Dashboard</h1>
|
||||
<p className="session-id">Session ID: <code>{sessionId}</code></p>
|
||||
<div className="session-id-container">
|
||||
<p className="session-id">
|
||||
Session ID: <code>{sessionId}</code>
|
||||
</p>
|
||||
<button className="btn-copy" onClick={copySessionId} title="Copy Session ID">
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>
|
||||
<p className="connection-status">
|
||||
<span className={`status-indicator ${isConnected ? 'connected' : 'disconnected'}`}>
|
||||
{isConnected ? '● Connected' : '○ Disconnected'}
|
||||
@@ -197,6 +316,136 @@ function StorytellerView({ sessionId }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contextual Response Generator */}
|
||||
<div className="contextual-section">
|
||||
<div className="contextual-header">
|
||||
<h3>🧠 AI Context-Aware Response Generator</h3>
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={() => setShowContextualGenerator(!showContextualGenerator)}
|
||||
>
|
||||
{showContextualGenerator ? '▼ Hide' : '▶ Show'} Generator
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showContextualGenerator && (
|
||||
<div className="contextual-generator">
|
||||
<p className="contextual-description">
|
||||
Generate a response that takes into account multiple characters' actions and messages.
|
||||
Perfect for creating scenes or responses that incorporate everyone's contributions.
|
||||
</p>
|
||||
|
||||
{/* Character Selection */}
|
||||
<div className="character-selection">
|
||||
<div className="selection-header">
|
||||
<h4>Select Characters to Include:</h4>
|
||||
<button className="btn-small" onClick={selectAllPending} disabled={pendingCount === 0}>
|
||||
Select All Pending ({pendingCount})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="character-checkboxes">
|
||||
{Object.entries(characters).map(([id, char]) => (
|
||||
<label key={id} className={`character-checkbox ${char.pending_response ? 'has-pending' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCharacterIds.includes(id)}
|
||||
onChange={() => toggleCharacterSelection(id)}
|
||||
/>
|
||||
<span className="checkbox-label">
|
||||
{char.name}
|
||||
{char.pending_response && <span className="pending-badge-small">●</span>}
|
||||
<span className="message-count">({char.conversation_history?.length || 0} msgs)</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedCharacterIds.length > 0 && (
|
||||
<div className="selection-summary">
|
||||
Selected: {selectedCharacterIds.map(id => characters[id]?.name).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Response Type */}
|
||||
<div className="response-type-selector">
|
||||
<label>
|
||||
<strong>Response Type:</strong>
|
||||
<select
|
||||
value={contextualResponseType}
|
||||
onChange={(e) => setContextualResponseType(e.target.value)}
|
||||
>
|
||||
<option value="scene">Scene Description (broadcast to all)</option>
|
||||
<option value="individual">Individual Responses (sent privately to each character)</option>
|
||||
</select>
|
||||
</label>
|
||||
{contextualResponseType === 'individual' && (
|
||||
<p className="response-type-help">
|
||||
💡 The AI will generate responses in this format: <code>[CharacterName] Response text here</code>. Each response is automatically parsed and sent privately to the respective character.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<div className="model-selector-contextual">
|
||||
<label>
|
||||
<strong>LLM Model:</strong>
|
||||
<select
|
||||
value={contextualModel}
|
||||
onChange={(e) => setContextualModel(e.target.value)}
|
||||
>
|
||||
<option value="gpt-4o">GPT-4o (Latest)</option>
|
||||
<option value="gpt-4-turbo">GPT-4 Turbo</option>
|
||||
<option value="gpt-4">GPT-4</option>
|
||||
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Additional Context */}
|
||||
<div className="additional-context">
|
||||
<label>
|
||||
<strong>Additional Context (optional):</strong>
|
||||
<textarea
|
||||
placeholder="Add any extra information or guidance for the AI (e.g., 'Make it dramatic', 'They should encounter danger', etc.)"
|
||||
value={contextualAdditionalContext}
|
||||
onChange={(e) => setContextualAdditionalContext(e.target.value)}
|
||||
rows="2"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<button
|
||||
className="btn-primary btn-large"
|
||||
onClick={generateContextualResponse}
|
||||
disabled={selectedCharacterIds.length === 0 || isGeneratingContextual || !isConnected}
|
||||
>
|
||||
{isGeneratingContextual ? '⏳ Generating...' : '✨ Generate Context-Aware Response'}
|
||||
</button>
|
||||
|
||||
{/* Generated Response */}
|
||||
{generatedContextualResponse && (
|
||||
<div className="generated-response">
|
||||
<h4>Generated Response:</h4>
|
||||
<div className="response-content">
|
||||
{generatedContextualResponse}
|
||||
</div>
|
||||
<div className="response-actions">
|
||||
<button className="btn-primary" onClick={useAsScene}>
|
||||
Use as Scene
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={() => setGeneratedContextualResponse('')}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="storyteller-content">
|
||||
<div className="character-list">
|
||||
<h3>Characters ({Object.keys(characters).length})</h3>
|
||||
|
||||
Reference in New Issue
Block a user