NovelCrafter / static /index.html
NoLev's picture
Update static/index.html
12ff4bf verified
raw
history blame
18.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Novel Prose Generator</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom styles for progress bar, copy button, and file upload */
.progress-bar {
width: 200px;
height: 20px;
background-color: #4b5563;
border-radius: 4px;
overflow: hidden;
display: none;
margin-left: 16px;
}
.progress-bar.active {
display: block;
}
.progress-bar-fill {
height: 100%;
background-color: #4f46e5;
width: 0%;
transition: width 0.1s ease-in-out;
animation: progress 2s linear infinite;
}
@keyframes progress {
0% { width: 0%; }
50% { width: 80%; }
100% { width: 0%; }
}
.progress-bar-text {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 230px;
color: #ffffff;
font-size: 0.875rem;
display: none;
}
.progress-bar-text.active {
display: block;
}
.tooltip {
position: relative;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 120px;
background-color: #4b5563;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.error-message {
color: #f87171;
font-size: 0.875rem;
margin-top: 0.25rem;
}
</style>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-gray-800 py-4 shadow-md">
<div class="container mx-auto px-4">
<h1 class="text-2xl font-bold">Novel Prose Generator</h1>
</div>
</header>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8 flex-grow">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Input Form -->
<div class="bg-gray-800 p-6 rounded-lg shadow-lg">
<h2 class="text-xl font-semibold mb-4">Input Details</h2>
<form id="proseForm" class="space-y-4" aria-label="Prose Generation Form">
<div>
<label for="projectId" class="block text-sm font-medium">Project ID</label>
<input type="text" id="projectId" name="projectId" value="Beyond the Cradle" required class="mt-1 block w-full bg-gray-700 border-gray-600 rounded-md p-2 text-gray-100 focus:ring-indigo-500 focus:border-indigo-500" aria-required="true">
<p id="projectIdError" class="error-message hidden">Project ID is required.</p>
</div>
<div>
<label for="model" class="block text-sm font-medium">Model</label>
<select id="model" name="model" class="mt-1 block w-full bg-gray-700 border-gray-600 rounded-md p-2 text-gray-100 focus:ring-indigo-500 focus:border-indigo-500" aria-label="Select AI model">
<option value="deepseek/deepseek-r1-0528:free">Deepseek(Free)</option>
<option value="anthropic/claude-3.5-sonnet">Claude 3.5 Sonnet</option>
<option value="openai/gpt-4o">GPT-4o</option>
<option value="meta-llama/llama-3.1-70b-instruct">LLaMA 3.1 70B Instruct</option>
</select>
</div>
<div>
<label for="manuscriptMode" class="block text-sm font-medium">Manuscript Mode</label>
<select id="manuscriptMode" name="manuscriptMode" class="mt-1 block w-full bg-gray-700 border-gray-600 rounded-md p-2 text-gray-100 focus:ring-indig
o-500 focus:border-indigo-500" aria-label="Select manuscript mode">
<option value="summary">Summary</option>
<option value="full">Full</option>
<option value="excerpts">Excerpts</option>
</select>
</div>
<div>
<label for="manuscript" class="block text-sm font-medium">Manuscript</label>
<textarea id="manuscript" name="manuscript" rows="6" maxlength="150000" class="mt-1 block w-full bg-gray-700 border-gray-600 rounded-md p-2 text-gray-100 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Paste your manuscript here (up to 150,000 characters)..." aria-describedby="manuscriptCharCount"></textarea>
<p id="manuscriptCharCount" class="text-sm text-gray-400 mt-1">Characters: <span id="manuscriptCount">0</span>/150,000</p>
<input type="file" id="manuscriptFile" accept=".txt" class="mt-2 block w-full text-gray-100" aria-label="Upload manuscript file">
<p id="manuscriptFileError" class="error-message hidden">File must be a .txt file under 150,000 characters.</p>
</div>
<div>
<label for="outline" class="block text-sm font-medium">Outline</label>
<textarea id="outline" name="outline" rows="4" class="mt-1 block w-full bg-gray-700 border-gray-600 rounded-md p-2 text-gray-100 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Enter your story outline..."></textarea>
</div>
<div>
<label for="characters" class="block text-sm font-medium">Characters</label>
<textarea id="characters" name="characters" rows="4" class="mt-1 block w-full bg-gray-700 border-gray-600 rounded-md p-2 text-gray-100 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Describe your characters..."></textarea>
</div>
<div>
<label for="prompt" class="block text-sm font-medium">Prompt</label>
<textarea id="prompt" name="prompt" rows="4" required class="mt-1 block w-full bg-gray-700 border-gray-600 rounded-md p-2 text-gray-100 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Enter your specific prose generation prompt..." aria-required="true"></textarea>
<p id="promptError" class="error-message hidden">Prompt is required.</p>
</div>
<div class="flex space-x-4">
<button type="submit" id="generateBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-md" aria-label="Generate prose">Generate Prose</button>
<button type="button" id="loadInputsBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-md" aria-label="Load latest inputs">Load Latest Inputs</button>
</div>
</form>
</div>
<!-- Output Section -->
<div class="bg-gray-800 p-6 rounded-lg shadow-lg">
<div class="flex items-center mb-4">
<h2 class="text-xl font-semibold">Generated Prose</h2>
<div id="progressBar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar-fill"></div>
</div>
<span id="progressText" class="progress-bar-text">Processing manuscript...</span>
<div class="tooltip ml-auto">
<button id="copyBtn" class="bg-gray-600 hover:bg-gray-700 text-white p-2 rounded-md hidden" aria-label="Copy generated prose">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
<span class="tooltip-text">Copy to Clipboard</span>
</div>
</div>
<div id="proseOutput" class="bg-gray-700 p-4 rounded-md text-gray-100 h-96 overflow-y-auto" role="region" aria-live="polite"></div>
<div id="errorOutput" class="text-red-400 mt-2 hidden" role="alert"></div>
<div id="tokenUsage" class="mt-4 text-sm text-gray-400 hidden">
Estimated Tokens Used: <span id="tokenCount">0</span>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-gray-800 py-4 mt-auto">
<div class="container mx-auto px-4 text-center text-gray-400">
<p>© 2025 Novel Prose Generator. Powered by Hugging Face and OpenRouter.</p>
</div>
</footer>
<script>
// Estimate token count (heuristic: 1 word ≈ 1.33 tokens for English)
function estimateTokens(text) {
if (!text) return 0;
const words = text.trim().split(/\s+/).length;
return Math.round(words * 1.33);
}
// Show progress bar
function showProgress(message = "Processing manuscript...") {
document.getElementById('progressText').textContent = message;
document.getElementById('progressBar').classList.add('active');
document.getElementById('progressText').classList.add('active');
}
// Hide progress bar
function hideProgress() {
document.getElementById('progressBar').classList.remove('active');
document.getElementById('progressText').classList.remove('active');
}
// Show error message
function showError(elementId, message) {
const errorElement = document.getElementById(elementId);
errorElement.textContent = message;
errorElement.classList.remove('hidden');
}
// Hide error message
function hideError(elementId) {
const errorElement = document.getElementById(elementId);
errorElement.classList.add('hidden');
}
// Copy prose to clipboard
async function copyProse() {
const proseOutput = document.getElementById('proseOutput').textContent;
if (!proseOutput) {
alert('No prose to copy!');
return;
}
try {
await navigator.clipboard.writeText(proseOutput);
const tooltipText = document.querySelector('.tooltip-text');
tooltipText.textContent = 'Copied!';
setTimeout(() => {
tooltipText.textContent = 'Copy to Clipboard';
}, 2000);
} catch (error) {
console.error('Error copying prose:', error);
alert('Failed to copy prose: ' + error.message);
}
}
// Update manuscript character count
function updateManuscriptCount() {
const manuscript = document.getElementById('manuscript').value;
document.getElementById('manuscriptCount').textContent = manuscript.length;
}
// Handle file upload
document.getElementById('manuscriptFile').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.name.endsWith('.txt')) {
showError('manuscriptFileError', 'File must be a .txt file.');
e.target.value = '';
return;
}
try {
const text = await file.text();
if (text.length > 150000) {
showError('manuscriptFileError', 'File exceeds 150,000 characters.');
e.target.value = '';
return;
}
document.getElementById('manuscript').value = text;
updateManuscriptCount();
hideError('manuscriptFileError');
} catch (error) {
showError('manuscriptFileError', 'Error reading file: ' + error.message);
e.target.value = '';
}
});
// Handle manuscript input
document.getElementById('manuscript').addEventListener('input', () => {
updateManuscriptCount();
hideError('manuscriptFileError');
});
// Validate form
function validateForm(data) {
let valid = true;
if (!data.projectId) {
showError('projectIdError', 'Project ID is required.');
valid = false;
} else {
hideError('projectIdError');
}
if (!data.prompt) {
showError('promptError', 'Prompt is required.');
valid = false;
} else {
hideError('promptError');
}
return valid;
}
// Handle form submission
document.getElementById('proseForm').addEventListener('submit', async (e) => {
e.preventDefault();
hideError('errorOutput');
const formData = new FormData(e.target);
const data = {
model: formData.get('model'),
manuscript: formData.get('manuscript'),
outline: formData.get('outline'),
characters: formData.get('characters'),
prompt: formData.get('prompt'),
projectId: formData.get('projectId'),
manuscriptMode: formData.get('manuscriptMode')
};
if (!validateForm(data)) {
return;
}
// Client-side manuscript chunking (send up to 50,000 chars to avoid server overload)
const maxChunkSize = 50000;
const manuscriptChunks = [];
for (let i = 0; i < data.manuscript.length; i += maxChunkSize) {
manuscriptChunks.push(data.manuscript.slice(i, i + maxChunkSize));
}
data.manuscript = manuscriptChunks[0] || ''; // Send first chunk only for now
showProgress();
try {
const response = await fetch('/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
const proseOutput = document.getElementById('proseOutput');
proseOutput.textContent = '';
let fullText = '';
// Stream the response
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
fullText += chunk;
proseOutput.textContent = fullText;
proseOutput.scrollTop = proseOutput.scrollHeight;
}
// Update token usage
const tokenCount = estimateTokens(fullText);
document.getElementById('tokenCount').textContent = tokenCount;
document.getElementById('tokenUsage').classList.remove('hidden');
// Show copy button
document.getElementById('copyBtn').classList.remove('hidden');
// Check for server-side errors (e.g., summarization errors)
if (fullText.includes('Error:') || fullText.includes('Failed to generate')) {
showError('errorOutput', 'Server error occurred. Check logs for details.');
}
} catch (error) {
console.error('Error generating prose:', error);
document.getElementById('proseOutput').textContent = `Error: ${error.message}`;
showError('errorOutput', error.message);
} finally {
hideProgress();
}
});
// Handle load latest inputs
document.getElementById('loadInputsBtn').addEventListener('click', async () => {
const projectId = document.getElementById('projectId').value;
if (!projectId) {
showError('projectIdError', 'Please enter a Project ID.');
return;
}
showProgress('Loading inputs...');
hideError('errorOutput');
try {
const response = await fetch(`/inputs/${encodeURIComponent(projectId)}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
document.getElementById('manuscript').value = data.manuscript || '';
updateManuscriptCount();
document.getElementById('outline').value = data.outline || '';
document.getElementById('characters').value = data.characters || '';
// Do not overwrite prompt to preserve user input
hideError('projectIdError');
} catch (error) {
console.error('Error loading inputs:', error);
showError('errorOutput', `Error loading inputs: ${error.message}`);
} finally {
hideProgress();
}
});
// Handle copy button click
document.getElementById('copyBtn').addEventListener('click', copyProse);
// Initialize manuscript character count
updateManuscriptCount();
</script>
</body>
</html>