ccl4/dialogBuilder.html
2025-06-13 16:39:38 +02:00

404 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dialogue Builder</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom CSS for a slightly better font */
body {
font-family: 'Inter', sans-serif;
}
/* Style for editable paragraphs */
.editable-line:focus {
outline: 2px solid theme('colors.blue.400');
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.5); /* blue-400 with opacity */
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen flex items-center justify-center p-4">
<div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-2xl border border-blue-200">
<h1 class="text-3xl font-bold text-center mb-6 text-gray-800">Dialogue Builder</h1>
<!-- Party Name Inputs -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label for="party1Name" class="block text-gray-700 text-sm font-medium mb-2">Party 1 Name:</label>
<input type="text" id="party1Name" value="Alice" placeholder="Enter name for Party 1" class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 shadow-sm">
</div>
<div>
<label for="party2Name" class="block text-gray-700 text-sm font-medium mb-2">Party 2 Name:</label>
<input type="text" id="party2Name" value="Bob" placeholder="Enter name for Party 2" class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 shadow-sm">
</div>
</div>
<!-- Dialogue Part Selection -->
<div class="mb-6">
<label class="block text-gray-700 text-sm font-medium mb-2">Dialogue Part:</label>
<div class="flex flex-wrap gap-4">
<label class="inline-flex items-center">
<input type="radio" name="dialoguePart" value="initial" class="form-radio text-blue-600 h-4 w-4" checked>
<span class="ml-2 text-gray-700">Initial Dialog</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="dialoguePart" value="reminder" class="form-radio text-green-600 h-4 w-4">
<span class="ml-2 text-gray-700">Reminder Dialog</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="dialoguePart" value="success" class="form-radio text-purple-600 h-4 w-4">
<span class="ml-2 text-gray-700">Success Dialog</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="dialoguePart" value="successRepetition" class="form-radio text-red-600 h-4 w-4">
<span class="ml-2 text-gray-700">Success Repetition</span>
</label>
</div>
</div>
<!-- Dialogue Input Section -->
<div class="mb-6">
<label for="dialogueInput" class="block text-gray-700 text-sm font-medium mb-2">Dialogue Line:</label>
<textarea id="dialogueInput" rows="3" placeholder="Type a dialogue line here..." class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 shadow-sm resize-y"></textarea>
</div>
<!-- Speaker Selection Buttons -->
<div class="flex flex-col sm:flex-row gap-4 mb-8">
<button id="addParty1Btn" class="flex-1 bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition duration-300 ease-in-out transform hover:scale-105">
Add as Party 1
</button>
<button id="addParty2Btn" class="flex-1 bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition duration-300 ease-in-out transform hover:scale-105">
Add as Party 2
</button>
</div>
<!-- Generated Dialogue Display -->
<h2 class="text-2xl font-semibold text-gray-800 mb-4">Conversation Preview:</h2>
<div id="conversationDisplay" class="bg-gray-50 p-4 rounded-lg border border-gray-200 min-h-[150px] max-h-[400px] overflow-y-auto mb-6 shadow-inner">
<!-- Dialogue lines will be appended here -->
<p class="text-gray-500 text-center italic">Start adding dialogue lines above!</p>
</div>
<!-- Load JSON Section -->
<h2 class="text-2xl font-semibold text-gray-800 mb-4">Load Dialogue from JSON:</h2>
<div class="mb-6">
<textarea id="jsonLoadInput" rows="5" placeholder="Paste your dialogue JSON here..." class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 shadow-sm resize-y"></textarea>
<div class="flex justify-center mt-4">
<button id="loadJsonBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105">
Load Dialogue from JSON
</button>
</div>
</div>
<!-- Generate JSON Button -->
<div class="flex justify-center mb-6">
<button id="generateJsonBtn" class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105">
Generate JSON
</button>
</div>
<!-- JSON Output Display -->
<h2 class="text-2xl font-semibold text-gray-800 mb-4">Generated JSON:</h2>
<div class="relative">
<textarea id="jsonOutput" rows="8" readonly class="w-full p-4 border border-gray-300 rounded-lg bg-gray-100 text-gray-800 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-purple-400 resize-y"></textarea>
<button id="copyJsonBtn" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-800 text-white text-xs font-semibold py-1.5 px-3 rounded-md shadow-sm transition duration-200">
Copy
</button>
</div>
<div id="messageBox" class="hidden mt-4 p-3 rounded-lg text-white text-center"></div>
</div>
<script>
// Array to store dialogue objects, now structured into parts
let dialogue = {
initial: [],
reminder: [],
success: [],
successRepetition: []
};
// Get references to HTML elements
const party1NameInput = document.getElementById('party1Name');
const party2NameInput = document.getElementById('party2Name');
const dialogueInput = document.getElementById('dialogueInput');
const addParty1Btn = document.getElementById('addParty1Btn');
const addParty2Btn = document.getElementById('addParty2Btn');
const conversationDisplay = document.getElementById('conversationDisplay');
const generateJsonBtn = document.getElementById('generateJsonBtn');
const jsonOutput = document.getElementById('jsonOutput');
const copyJsonBtn = document.getElementById('copyJsonBtn');
const messageBox = document.getElementById('messageBox');
const dialoguePartRadios = document.querySelectorAll('input[name="dialoguePart"]');
const jsonLoadInput = document.getElementById('jsonLoadInput');
const loadJsonBtn = document.getElementById('loadJsonBtn');
/**
* Displays a temporary message in the message box.
* @param {string} message - The message to display.
* @param {string} type - The type of message ('success', 'error', 'info').
*/
function showMessage(message, type) {
messageBox.textContent = message;
messageBox.className = 'mt-4 p-3 rounded-lg text-white text-center'; // Reset classes
if (type === 'success') {
messageBox.classList.add('bg-green-500');
} else if (type === 'error') {
messageBox.classList.add('bg-red-500');
} else { // Default to info
messageBox.classList.add('bg-blue-500');
}
messageBox.classList.remove('hidden');
setTimeout(() => {
messageBox.classList.add('hidden');
}, 3000); // Hide after 3 seconds
}
/**
* Gets the currently selected dialogue part from the radio buttons.
* @returns {string} The value of the selected radio button (e.g., 'initial', 'reminder').
*/
function getSelectedDialoguePart() {
for (const radio of dialoguePartRadios) {
if (radio.checked) {
return radio.value;
}
}
return 'initial'; // Default to initial if nothing is checked (shouldn't happen with default check)
}
/**
* Adds a dialogue line to the conversation.
* @param {string} speakerName - The name of the speaker.
*/
function addDialogueLine(speakerName) {
const lineText = dialogueInput.value.trim();
if (lineText === '') {
showMessage('Please enter some text for the dialogue.', 'error');
return;
}
const selectedPart = getSelectedDialoguePart();
dialogue[selectedPart].push({
// Wrap the speaker name with <b> tags for JSON output
speaker: `<b>${speakerName}</b>`,
text: lineText
});
// Clear the input field
dialogueInput.value = '';
// Update the conversation display
renderConversation();
}
/**
* Renders the current dialogue array to the conversation display area.
* It now organizes dialogue by the four defined parts, and includes edit/delete buttons.
*/
function renderConversation() {
conversationDisplay.innerHTML = ''; // Clear previous content
let hasContent = false; // Flag to check if any dialogue exists
const partTitles = {
initial: 'Initial Dialog',
reminder: 'Reminder Dialog',
success: 'Success Dialog',
successRepetition: 'Success Repetition'
};
// Iterate over each dialogue part
for (const partKey in dialogue) {
if (dialogue[partKey].length > 0) {
hasContent = true;
// Add a heading for each part
const heading = document.createElement('h3');
heading.classList.add('text-lg', 'font-semibold', 'text-gray-700', 'mb-2', 'mt-4', 'border-b-2', 'border-blue-300', 'pb-1');
heading.textContent = partTitles[partKey];
conversationDisplay.appendChild(heading);
// Append each line within the current part
dialogue[partKey].forEach((line, index) => {
const lineWrapper = document.createElement('div');
lineWrapper.classList.add('flex', 'items-start', 'mb-2', 'gap-2'); // Flex container for line and button
const p = document.createElement('p');
p.classList.add(
'flex-1', // Allows paragraph to grow and fill space
'p-2',
'rounded-md',
'shadow-sm',
'text-gray-800',
'editable-line', // Custom class for styling editable state
'focus:outline-none', // Tailwind class for no outline on focus
'focus:ring-2', // Tailwind class for ring on focus
'focus:ring-blue-400' // Tailwind class for blue ring on focus
);
p.contentEditable = true; // Make the paragraph editable
p.dataset.part = partKey; // Store the part key
p.dataset.index = index; // Store the index within the part array
// Apply different styles based on the speaker (checking against un-bolded names for comparison)
if (line.speaker.replace(/<\/?b>/g, '') === party1NameInput.value) { // Remove <b> tags for comparison
p.classList.add('bg-blue-100', 'self-start', 'mr-auto');
} else if (line.speaker.replace(/<\/?b>/g, '') === party2NameInput.value) { // Remove <b> tags for comparison
p.classList.add('bg-green-100', 'self-end', 'ml-auto');
lineWrapper.classList.add('justify-end'); // Align wrapper to end for party 2
} else {
// Fallback for speaker names that might not match inputs or have other formatting
p.classList.add('bg-gray-100');
}
// Set innerHTML directly, allowing the <b> tag from the 'speaker' property to render
p.innerHTML = `${line.speaker}: <span class="line-text">${line.text}</span>`;
// Add blur event listener for editing
p.addEventListener('blur', (event) => {
const editedText = event.target.querySelector('.line-text').textContent.trim();
const part = event.target.dataset.part;
const idx = parseInt(event.target.dataset.index);
if (dialogue[part] && dialogue[part][idx]) {
dialogue[part][idx].text = editedText;
// No need to re-render the whole conversation for just text change,
// but we do if an index or order changes.
// showMessage('Dialogue line updated!', 'success'); // Optional: show message
}
});
// Add delete button
const deleteBtn = document.createElement('button');
deleteBtn.classList.add(
'p-1', 'bg-red-400', 'hover:bg-red-500', 'text-white',
'rounded-full', 'w-6', 'h-6', 'flex', 'items-center', 'justify-center',
'text-xs', 'font-bold', 'transition', 'duration-200', 'flex-shrink-0'
);
deleteBtn.textContent = 'X';
deleteBtn.title = 'Delete this line';
deleteBtn.addEventListener('click', () => {
const part = p.dataset.part;
const idx = parseInt(p.dataset.index);
if (dialogue[part] && dialogue[part][idx]) {
dialogue[part].splice(idx, 1); // Remove the line
renderConversation(); // Re-render to update indices and display
showMessage('Dialogue line deleted!', 'success');
}
});
lineWrapper.appendChild(p);
lineWrapper.appendChild(deleteBtn);
conversationDisplay.appendChild(lineWrapper);
});
}
}
// Display placeholder if no dialogue has been added yet
if (!hasContent) {
conversationDisplay.innerHTML = '<p class="text-gray-500 text-center italic">Start adding dialogue lines above!</p>';
} else {
// Scroll to the bottom to show the latest dialogue if content exists
conversationDisplay.scrollTop = conversationDisplay.scrollHeight;
}
}
/**
* Converts the dialogue object to a JSON string and displays it.
* The JSON now reflects the structured parts of the dialogue.
*/
function generateJson() {
// Check if any dialogue parts have content
const hasContent = Object.values(dialogue).some(part => part.length > 0);
if (!hasContent) {
jsonOutput.value = '{}'; // Output an empty object if no dialogue
showMessage('No dialogue lines to generate JSON from.', 'info');
return;
}
jsonOutput.value = JSON.stringify(dialogue, null, 2); // Pretty print JSON
showMessage('JSON generated successfully!', 'success');
}
/**
* Copies the content of the JSON output textarea to the clipboard.
*/
function copyJsonToClipboard() {
jsonOutput.select();
document.execCommand('copy');
showMessage('JSON copied to clipboard!', 'success');
}
/**
* Loads dialogue data from the JSON input textarea.
*/
function loadJson() {
const jsonString = jsonLoadInput.value.trim();
if (jsonString === '') {
showMessage('Please paste JSON data into the input field.', 'error');
return;
}
try {
const loadedData = JSON.parse(jsonString);
// Basic validation: check if it's an object and has the expected keys
if (typeof loadedData !== 'object' || loadedData === null) {
showMessage('Invalid JSON format. Expected an object.', 'error');
return;
}
const expectedKeys = ['initial', 'reminder', 'success', 'successRepetition'];
let isValid = true;
let newDialogue = {
initial: [],
reminder: [],
success: [],
successRepetition: []
};
for (const key of expectedKeys) {
if (Array.isArray(loadedData[key])) {
// Validate each item in the array
const validLines = loadedData[key].filter(item =>
typeof item === 'object' && item !== null &&
typeof item.speaker === 'string' && typeof item.text === 'string'
);
newDialogue[key] = validLines;
} else if (loadedData[key] !== undefined) {
isValid = false; // Key exists but is not an array
break;
}
// If key is missing, it's fine, we'll keep its array empty in newDialogue
}
if (!isValid) {
showMessage('Invalid JSON structure. Ensure dialogue parts are arrays with speaker and text.', 'error');
return;
}
dialogue = newDialogue; // Replace current dialogue with loaded valid data
renderConversation();
showMessage('Dialogue loaded successfully!', 'success');
jsonLoadInput.value = ''; // Clear the load input
} catch (e) {
showMessage('Error parsing JSON: ' + e.message, 'error');
}
}
// Event Listeners
addParty1Btn.addEventListener('click', () => addDialogueLine(party1NameInput.value));
// Corrected: changed party22NameInput to party2NameInput
addParty2Btn.addEventListener('click', () => addDialogueLine(party2NameInput.value));
generateJsonBtn.addEventListener('click', generateJson);
copyJsonBtn.addEventListener('click', copyJsonToClipboard);
loadJsonBtn.addEventListener('click', loadJson);
// Initial render (empty)
renderConversation();
</script>
</body>
</html>