reader / index.html
Toowired's picture
Add 3 files
72f32d0 verified
Raw
History Blame
83.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HD TTS Document Reader | Natural Human Voices</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #f8fafc;
}
.waveform {
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
border-radius: 8px;
position: relative;
overflow: hidden;
}
.waveform-bar {
width: 3px;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 3px;
animation: equalize 1.5s infinite ease-in-out;
animation-delay: calc(var(--delay) * 0.1s);
}
@keyframes equalize {
0%, 100% { height: 10px; }
50% { height: 30px; }
}
.document-container {
max-height: 400px;
overflow-y: auto;
scrollbar-width: thin;
}
.document-container::-webkit-scrollbar {
width: 6px;
}
.document-container::-webkit-scrollbar-track {
background: #f1f5f9;
}
.document-container::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 20px;
}
.highlight {
background-color: #fef08a;
transition: background-color 0.3s ease;
}
.voice-option:hover {
transform: scale(1.03);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.progress-bar {
height: 4px;
background-color: #e2e8f0;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #3b82f6;
width: 0%;
transition: width 0.1s linear;
}
.settings-panel {
transition: all 0.3s ease;
max-height: 0;
overflow: hidden;
}
.settings-panel.open {
max-height: 500px;
}
.tab-active {
border-bottom: 2px solid #3b82f6;
color: #3b82f6;
font-weight: 500;
}
.language-flag {
width: 24px;
height: 16px;
display: inline-block;
margin-right: 8px;
background-size: cover;
border-radius: 2px;
}
/* Language flags */
.flag-us {
background-image: url('https://flagcdn.com/us.svg');
}
.flag-uk {
background-image: url('https://flagcdn.com/gb.svg');
}
.flag-au {
background-image: url('https://flagcdn.com/au.svg');
}
.flag-ca {
background-image: url('https://flagcdn.com/ca.svg');
}
.flag-in {
background-image: url('https://flagcdn.com/in.svg');
}
.flag-za {
background-image: url('https://flagcdn.com/za.svg');
}
.flag-ie {
background-image: url('https://flagcdn.com/ie.svg');
}
.flag-nz {
background-image: url('https://flagcdn.com/nz.svg');
}
.typing-cursor {
display: inline-block;
width: 2px;
height: 1em;
background-color: #3b82f6;
animation: blink 1s infinite;
vertical-align: middle;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.document-toolbar {
transition: all 0.3s ease;
}
.document-toolbar.hidden {
opacity: 0;
transform: translateY(-10px);
pointer-events: none;
}
</style>
</head>
<body class="min-h-screen bg-gray-50">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<header class="mb-8 text-center">
<h1 class="text-4xl font-bold text-gray-800 mb-2">HD TTS Document Reader</h1>
<p class="text-lg text-gray-600">Natural human voices with long context support using Google's free HD voices</p>
<div class="mt-4 flex justify-center gap-4">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
<i class="fas fa-microphone mr-2"></i> HD Voices
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800">
<i class="fas fa-cloud mr-2"></i> Val Town Storage
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
<i class="fas fa-bolt mr-2"></i> Serverless
</span>
</div>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Document Input Section -->
<div class="lg:col-span-2 bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold text-gray-800">Document Reader</h2>
<div class="flex space-x-2">
<button id="settings-toggle" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
<i class="fas fa-cog"></i>
</button>
<button id="fullscreen-toggle" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<div id="settings-panel" class="settings-panel mb-6 bg-gray-50 p-4 rounded-lg">
<div class="flex border-b mb-4">
<button class="tab-btn tab-active px-4 py-2" data-tab="formatting">Formatting</button>
<button class="tab-btn px-4 py-2" data-tab="pronunciation">Pronunciation</button>
<button class="tab-btn px-4 py-2" data-tab="advanced">Advanced</button>
</div>
<div id="formatting-tab" class="tab-content">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Paragraph Spacing</label>
<select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
<option>Single</option>
<option>1.5 lines</option>
<option>Double</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Font Size</label>
<select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
<option>Small</option>
<option selected>Medium</option>
<option>Large</option>
<option>Extra Large</option>
</select>
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Text Alignment</label>
<div class="flex space-x-2">
<button class="p-2 border rounded-md hover:bg-gray-100">
<i class="fas fa-align-left"></i>
</button>
<button class="p-2 border rounded-md hover:bg-gray-100">
<i class="fas fa-align-center"></i>
</button>
<button class="p-2 border rounded-md hover:bg-gray-100">
<i class="fas fa-align-right"></i>
</button>
<button class="p-2 border rounded-md hover:bg-gray-100">
<i class="fas fa-align-justify"></i>
</button>
</div>
</div>
</div>
<div id="pronunciation-tab" class="tab-content hidden">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Phonetic Adjustments</label>
<div class="space-y-2">
<div class="flex items-center">
<input type="checkbox" id="phonetic-help" class="mr-2">
<label for="phonetic-help" class="text-sm">Enable phonetic assistance</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="auto-pronounce" class="mr-2" checked>
<label for="auto-pronounce" class="text-sm">Auto-correct pronunciation</label>
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Custom Pronunciations</label>
<div class="flex items-center mb-2">
<input type="text" placeholder="Word" class="border border-gray-300 rounded-md px-3 py-1 text-sm mr-2 w-32">
<input type="text" placeholder="Pronunciation" class="border border-gray-300 rounded-md px-3 py-1 text-sm mr-2 w-32">
<button class="text-blue-600 hover:text-blue-800 text-sm">
<i class="fas fa-plus"></i> Add
</button>
</div>
<div class="text-xs text-gray-500">
Add custom pronunciations for specific words or names
</div>
</div>
</div>
<div id="advanced-tab" class="tab-content hidden">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Audio Quality</label>
<select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
<option>Standard (16kHz)</option>
<option selected>High (24kHz)</option>
<option>HD (32kHz)</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Chunk Size</label>
<select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
<option>Small (better for slow connections)</option>
<option selected>Medium (balanced)</option>
<option>Large (fewer requests)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Cache Duration</label>
<select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
<option>No caching</option>
<option selected>1 hour</option>
<option>1 day</option>
<option>1 week</option>
</select>
</div>
</div>
</div>
<div class="document-toolbar mb-2 flex flex-wrap gap-1">
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="bold" title="Bold">
<i class="fas fa-bold"></i>
</button>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="italic" title="Italic">
<i class="fas fa-italic"></i>
</button>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="underline" title="Underline">
<i class="fas fa-underline"></i>
</button>
<div class="w-px h-6 bg-gray-300 mx-1"></div>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="insertUnorderedList" title="Bullet List">
<i class="fas fa-list-ul"></i>
</button>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="insertOrderedList" title="Numbered List">
<i class="fas fa-list-ol"></i>
</button>
<div class="w-px h-6 bg-gray-300 mx-1"></div>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="outdent" title="Decrease Indent">
<i class="fas fa-outdent"></i>
</button>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="indent" title="Increase Indent">
<i class="fas fa-indent"></i>
</button>
<div class="w-px h-6 bg-gray-300 mx-1"></div>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="createLink" title="Insert Link">
<i class="fas fa-link"></i>
</button>
<button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="unlink" title="Remove Link">
<i class="fas fa-unlink"></i>
</button>
</div>
<div class="mb-4">
<label for="document-text" class="block text-sm font-medium text-gray-700 mb-2">Document Text</label>
<div class="document-container bg-gray-50 p-4 rounded-lg border border-gray-200">
<div id="document-editor" class="min-h-[200px] outline-none" contenteditable="true" data-placeholder="Type or paste your text here..."></div>
<div id="document-content" class="hidden prose max-w-none"></div>
</div>
</div>
<div class="flex justify-between items-center">
<div class="text-sm text-gray-500">
<span id="word-count">0</span> words • <span id="char-count">0</span> characters
</div>
<div>
<button id="clear-btn" class="text-gray-500 hover:text-gray-700 mr-3">
<i class="fas fa-trash-alt mr-1"></i> Clear
</button>
<button id="spellcheck-btn" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-spell-check mr-1"></i> Spellcheck
</button>
</div>
</div>
</div>
</div>
<!-- Voice Controls Section -->
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">Voice Settings</h2>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Select Language & Voice</label>
<div class="space-y-3">
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-blue-50 border-gray-200" data-voice="en-US-Wavenet-A" data-lang="en-US">
<div class="flex items-center">
<span class="language-flag flag-us"></span>
<div class="flex-1">
<div class="font-medium">James (Standard)</div>
<div class="text-xs text-gray-500">Male • Wavenet A</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-purple-50 border-gray-200" data-voice="en-US-Wavenet-F" data-lang="en-US">
<div class="flex items-center">
<span class="language-flag flag-us"></span>
<div class="flex-1">
<div class="font-medium">Emily (Standard)</div>
<div class="text-xs text-gray-500">Female • Wavenet F</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-green-50 border-gray-200" data-voice="en-GB-Wavenet-B" data-lang="en-GB">
<div class="flex items-center">
<span class="language-flag flag-uk"></span>
<div class="flex-1">
<div class="font-medium">Oliver (British)</div>
<div class="text-xs text-gray-500">Male • Wavenet B</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-pink-50 border-gray-200" data-voice="en-AU-Wavenet-A" data-lang="en-AU">
<div class="flex items-center">
<span class="language-flag flag-au"></span>
<div class="flex-1">
<div class="font-medium">Charlotte (Australian)</div>
<div class="text-xs text-gray-500">Female • Wavenet A</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<button id="show-more-voices" class="w-full text-center text-blue-600 hover:text-blue-800 text-sm py-2">
<i class="fas fa-chevron-down mr-1"></i> Show More Voices
</button>
<div id="more-voices" class="hidden space-y-3">
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-yellow-50 border-gray-200" data-voice="en-IN-Wavenet-A" data-lang="en-IN">
<div class="flex items-center">
<span class="language-flag flag-in"></span>
<div class="flex-1">
<div class="font-medium">Arjun (Indian)</div>
<div class="text-xs text-gray-500">Male • Wavenet A</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-red-50 border-gray-200" data-voice="en-ZA-Wavenet-A" data-lang="en-ZA">
<div class="flex items-center">
<span class="language-flag flag-za"></span>
<div class="flex-1">
<div class="font-medium">Lebo (South African)</div>
<div class="text-xs text-gray-500">Female • Wavenet A</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-indigo-50 border-gray-200" data-voice="en-IE-Wavenet-A" data-lang="en-IE">
<div class="flex items-center">
<span class="language-flag flag-ie"></span>
<div class="flex-1">
<div class="font-medium">Connor (Irish)</div>
<div class="text-xs text-gray-500">Male • Wavenet A</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-teal-50 border-gray-200" data-voice="en-NZ-Wavenet-A" data-lang="en-NZ">
<div class="flex items-center">
<span class="language-flag flag-nz"></span>
<div class="flex-1">
<div class="font-medium">Tiana (New Zealand)</div>
<div class="text-xs text-gray-500">Female • Wavenet A</div>
</div>
<i class="fas fa-check text-blue-600 opacity-0 voice-check"></i>
</div>
</div>
<button id="show-less-voices" class="w-full text-center text-blue-600 hover:text-blue-800 text-sm py-2">
<i class="fas fa-chevron-up mr-1"></i> Show Fewer Voices
</button>
</div>
</div>
</div>
<div class="mb-6">
<label for="voice-speed" class="block text-sm font-medium text-gray-700 mb-2">Speed: <span id="speed-value">1.0</span>x</label>
<input type="range" id="voice-speed" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="mb-6">
<label for="voice-pitch" class="block text-sm font-medium text-gray-700 mb-2">Pitch: <span id="pitch-value">0</span></label>
<input type="range" id="voice-pitch" min="-20" max="20" step="1" value="0" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Reading Mode</label>
<div class="grid grid-cols-3 gap-2">
<button class="reading-mode-btn py-2 px-3 border rounded-lg bg-blue-50 border-blue-200 text-blue-700" data-mode="normal">
<i class="fas fa-book-open mr-1"></i> Normal
</button>
<button class="reading-mode-btn py-2 px-3 border rounded-lg border-gray-200 hover:bg-gray-50" data-mode="word">
<i class="fas fa-font mr-1"></i> Word-by-word
</button>
<button class="reading-mode-btn py-2 px-3 border rounded-lg border-gray-200 hover:bg-gray-50" data-mode="sentence">
<i class="fas fa-align-left mr-1"></i> Sentence
</button>
</div>
</div>
<div class="flex space-x-3 mb-4">
<button id="play-btn" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition flex items-center justify-center">
<i class="fas fa-play mr-2"></i> Play
</button>
<button id="pause-btn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-3 px-4 rounded-lg transition flex items-center justify-center" disabled>
<i class="fas fa-pause mr-2"></i> Pause
</button>
<button id="stop-btn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-3 px-4 rounded-lg transition flex items-center justify-center" disabled>
<i class="fas fa-stop mr-2"></i> Stop
</button>
</div>
<div class="progress-bar mb-1">
<div id="progress-fill" class="progress-fill"></div>
</div>
<div class="flex justify-between text-sm text-gray-500 mb-4">
<span id="current-time">0:00</span>
<span id="total-time">0:00</span>
</div>
<div class="flex justify-between">
<button id="skip-back-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Skip Back 15s">
<i class="fas fa-backward"></i>
</button>
<button id="rewind-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Rewind">
<i class="fas fa-undo"></i>
</button>
<button id="forward-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Forward">
<i class="fas fa-redo"></i>
</button>
<button id="skip-forward-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Skip Forward 15s">
<i class="fas fa-forward"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Audio Visualization -->
<div class="mt-8 bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold text-gray-800">Audio Visualization</h2>
<div class="flex items-center space-x-2">
<button id="visualization-mode" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Change Visualization">
<i class="fas fa-sliders-h"></i>
</button>
<button id="download-audio" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Download Audio">
<i class="fas fa-download"></i>
</button>
</div>
</div>
<div class="waveform" id="waveform">
<div class="waveform-bar" style="--delay: 1; height: 15px;"></div>
<div class="waveform-bar" style="--delay: 2; height: 25px;"></div>
<div class="waveform-bar" style="--delay: 3; height: 10px;"></div>
<div class="waveform-bar" style="--delay: 4; height: 30px;"></div>
<div class="waveform-bar" style="--delay: 5; height: 20px;"></div>
<div class="waveform-bar" style="--delay: 6; height: 15px;"></div>
<div class="waveform-bar" style="--delay: 7; height: 25px;"></div>
<div class="waveform-bar" style="--delay: 8; height: 10px;"></div>
<div class="waveform-bar" style="--delay: 9; height: 30px;"></div>
<div class="waveform-bar" style="--delay: 10; height: 20px;"></div>
</div>
<div class="mt-4 flex justify-center">
<button id="record-voice-btn" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center">
<i class="fas fa-microphone mr-2"></i> Record Your Voice
</button>
</div>
</div>
</div>
<!-- Saved Documents Section -->
<div class="mt-8 bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold text-gray-800">Saved Documents</h2>
<div class="flex space-x-2">
<button id="refresh-docs-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Refresh">
<i class="fas fa-sync-alt"></i>
</button>
<button id="save-doc-btn" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition">
<i class="fas fa-save mr-2"></i> Save Current
</button>
</div>
</div>
<div class="mb-4 flex items-center">
<div class="relative flex-1 max-w-md">
<input type="text" id="search-docs" placeholder="Search documents..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search text-gray-400"></i>
</div>
</div>
<button id="filter-docs-btn" class="ml-2 p-2 text-gray-500 hover:text-gray-700 rounded-lg hover:bg-gray-100">
<i class="fas fa-filter"></i>
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button class="flex items-center sort-btn" data-sort="title">
Title <i class="fas fa-sort ml-1"></i>
</button>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button class="flex items-center sort-btn" data-sort="date">
Date <i class="fas fa-sort ml-1"></i>
</button>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button class="flex items-center sort-btn" data-sort="length">
Length <i class="fas fa-sort ml-1"></i>
</button>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<button class="flex items-center sort-btn" data-sort="language">
Language <i class="fas fa-sort ml-1"></i>
</button>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody id="saved-docs-list" class="bg-white divide-y divide-gray-200">
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500">No saved documents yet</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-500">
Showing <span id="showing-from">1</span> to <span id="showing-to">0</span> of <span id="total-docs">0</span> documents
</div>
<div class="flex space-x-2">
<button id="prev-page-btn" class="px-3 py-1 border rounded-lg disabled:opacity-50" disabled>
<i class="fas fa-chevron-left"></i>
</button>
<button id="next-page-btn" class="px-3 py-1 border rounded-lg disabled:opacity-50" disabled>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<div id="url-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 max-w-md w-full">
<h3 class="text-xl font-semibold mb-4">Load Document from URL</h3>
<input type="text" id="document-url" placeholder="Enter document URL" class="w-full p-3 border border-gray-300 rounded-lg mb-4">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Document Type</label>
<select class="w-full border border-gray-300 rounded-md px-3 py-2">
<option>Auto-detect</option>
<option>Plain Text</option>
<option>Web Page</option>
<option>PDF</option>
<option>Word Document</option>
</select>
</div>
<div class="flex justify-end space-x-3">
<button id="cancel-url-btn" class="px-4 py-2 border border-gray-300 rounded-lg">Cancel</button>
<button id="load-url-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg">Load</button>
</div>
</div>
</div>
<div id="save-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 max-w-md w-full">
<h3 class="text-xl font-semibold mb-4">Save Document</h3>
<input type="text" id="document-title" placeholder="Document title" class="w-full p-3 border border-gray-300 rounded-lg mb-4">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Language</label>
<select class="w-full border border-gray-300 rounded-md px-3 py-2">
<option>English (United States)</option>
<option>English (United Kingdom)</option>
<option>English (Australia)</option>
<option>English (India)</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
<input type="text" id="document-tags" placeholder="Add tags (comma separated)" class="w-full p-3 border border-gray-300 rounded-lg">
</div>
<div class="flex justify-end space-x-3">
<button id="cancel-save-btn" class="px-4 py-2 border border-gray-300 rounded-lg">Cancel</button>
<button id="confirm-save-btn" class="px-4 py-2 bg-green-600 text-white rounded-lg">Save</button>
</div>
</div>
</div>
<div id="record-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 max-w-md w-full">
<h3 class="text-xl font-semibold mb-4">Record Your Voice</h3>
<div class="text-center mb-6">
<div class="w-32 h-32 mx-auto rounded-full bg-red-100 flex items-center justify-center mb-4">
<i class="fas fa-microphone text-red-600 text-4xl"></i>
</div>
<div id="record-status" class="text-lg font-medium">Ready to record</div>
<div id="record-timer" class="text-2xl font-bold my-2">00:00</div>
<div class="text-sm text-gray-500">Speak clearly into your microphone</div>
</div>
<div class="flex justify-center space-x-4">
<button id="start-record-btn" class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center">
<i class="fas fa-circle mr-2"></i> Start
</button>
<button id="stop-record-btn" class="px-6 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-lg flex items-center" disabled>
<i class="fas fa-stop mr-2"></i> Stop
</button>
<button id="play-record-btn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center" disabled>
<i class="fas fa-play mr-2"></i> Play
</button>
</div>
<div class="mt-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Save Recording As</label>
<div class="flex">
<input type="text" id="recording-name" placeholder="Recording name" class="flex-1 p-2 border border-gray-300 rounded-l-lg">
<button id="save-record-btn" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-r-lg">
<i class="fas fa-save mr-1"></i> Save
</button>
</div>
</div>
<div class="mt-4 flex justify-end">
<button id="close-record-btn" class="px-4 py-2 text-gray-600 hover:text-gray-800">
Close
</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const documentEditor = document.getElementById('document-editor');
const documentContent = document.getElementById('document-content');
const wordCount = document.getElementById('word-count');
const charCount = document.getElementById('char-count');
const playBtn = document.getElementById('play-btn');
const pauseBtn = document.getElementById('pause-btn');
const stopBtn = document.getElementById('stop-btn');
const clearBtn = document.getElementById('clear-btn');
const spellcheckBtn = document.getElementById('spellcheck-btn');
const formatBtns = document.querySelectorAll('.format-btn');
const settingsToggle = document.getElementById('settings-toggle');
const settingsPanel = document.getElementById('settings-panel');
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
const fullscreenToggle = document.getElementById('fullscreen-toggle');
const voiceSpeed = document.getElementById('voice-speed');
const voicePitch = document.getElementById('voice-pitch');
const speedValue = document.getElementById('speed-value');
const pitchValue = document.getElementById('pitch-value');
const progressFill = document.getElementById('progress-fill');
const currentTime = document.getElementById('current-time');
const totalTime = document.getElementById('total-time');
const waveform = document.getElementById('waveform');
const voiceOptions = document.querySelectorAll('.voice-option');
const showMoreVoices = document.getElementById('show-more-voices');
const showLessVoices = document.getElementById('show-less-voices');
const moreVoices = document.getElementById('more-voices');
const readingModeBtns = document.querySelectorAll('.reading-mode-btn');
const skipBackBtn = document.getElementById('skip-back-btn');
const rewindBtn = document.getElementById('rewind-btn');
const forwardBtn = document.getElementById('forward-btn');
const skipForwardBtn = document.getElementById('skip-forward-btn');
const visualizationMode = document.getElementById('visualization-mode');
const downloadAudio = document.getElementById('download-audio');
const recordVoiceBtn = document.getElementById('record-voice-btn');
const savedDocsList = document.getElementById('saved-docs-list');
const saveDocBtn = document.getElementById('save-doc-btn');
const refreshDocsBtn = document.getElementById('refresh-docs-btn');
const searchDocs = document.getElementById('search-docs');
const filterDocsBtn = document.getElementById('filter-docs-btn');
const sortBtns = document.querySelectorAll('.sort-btn');
const prevPageBtn = document.getElementById('prev-page-btn');
const nextPageBtn = document.getElementById('next-page-btn');
const showingFrom = document.getElementById('showing-from');
const showingTo = document.getElementById('showing-to');
const totalDocs = document.getElementById('total-docs');
const urlModal = document.getElementById('url-modal');
const documentUrl = document.getElementById('document-url');
const cancelUrlBtn = document.getElementById('cancel-url-btn');
const loadUrlBtn = document.getElementById('load-url-btn');
const saveModal = document.getElementById('save-modal');
const documentTitle = document.getElementById('document-title');
const cancelSaveBtn = document.getElementById('cancel-save-btn');
const confirmSaveBtn = document.getElementById('confirm-save-btn');
const recordModal = document.getElementById('record-modal');
const startRecordBtn = document.getElementById('start-record-btn');
const stopRecordBtn = document.getElementById('stop-record-btn');
const playRecordBtn = document.getElementById('play-record-btn');
const saveRecordBtn = document.getElementById('save-record-btn');
const closeRecordBtn = document.getElementById('close-record-btn');
const recordStatus = document.getElementById('record-status');
const recordTimer = document.getElementById('record-timer');
const recordingName = document.getElementById('recording-name');
// State variables
let selectedVoice = 'en-US-Wavenet-A';
let selectedLanguage = 'en-US';
let isPlaying = false;
let currentPosition = 0;
let speechSynthesis = window.speechSynthesis;
let utterance = null;
let savedDocuments = [];
let currentPage = 1;
const docsPerPage = 5;
let sortField = 'date';
let sortDirection = 'desc';
let isRecording = false;
let mediaRecorder;
let audioChunks = [];
let recordStartTime;
let recordInterval;
let isFullscreen = false;
let documentToolbarVisible = true;
// Initialize
updateCounts();
loadSavedDocuments();
setupDocumentEditor();
setupVoiceOptions();
// Event Listeners
documentEditor.addEventListener('input', updateCounts);
documentEditor.addEventListener('focus', showDocumentToolbar);
documentEditor.addEventListener('blur', hideDocumentToolbarAfterDelay);
playBtn.addEventListener('click', playTTS);
pauseBtn.addEventListener('click', pauseTTS);
stopBtn.addEventListener('click', stopTTS);
clearBtn.addEventListener('click', clearDocument);
spellcheckBtn.addEventListener('click', toggleSpellcheck);
formatBtns.forEach(btn => {
btn.addEventListener('click', () => {
const command = btn.dataset.command;
document.execCommand(command, false, null);
documentEditor.focus();
});
});
settingsToggle.addEventListener('click', toggleSettingsPanel);
fullscreenToggle.addEventListener('click', toggleFullscreen);
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
switchTab(tab);
});
});
voiceSpeed.addEventListener('input', () => {
speedValue.textContent = voiceSpeed.value;
updateUtteranceSettings();
});
voicePitch.addEventListener('input', () => {
pitchValue.textContent = voicePitch.value;
updateUtteranceSettings();
});
showMoreVoices.addEventListener('click', () => {
moreVoices.classList.remove('hidden');
showMoreVoices.classList.add('hidden');
});
showLessVoices.addEventListener('click', () => {
moreVoices.classList.add('hidden');
showMoreVoices.classList.remove('hidden');
});
readingModeBtns.forEach(btn => {
btn.addEventListener('click', () => {
readingModeBtns.forEach(b => {
b.classList.remove('bg-blue-50', 'border-blue-200', 'text-blue-700');
b.classList.add('border-gray-200', 'hover:bg-gray-50');
});
btn.classList.add('bg-blue-50', 'border-blue-200', 'text-blue-700');
btn.classList.remove('border-gray-200', 'hover:bg-gray-50');
});
});
skipBackBtn.addEventListener('click', () => skipTime(-15));
rewindBtn.addEventListener('click', () => skipTime(-5));
forwardBtn.addEventListener('click', () => skipTime(5));
skipForwardBtn.addEventListener('click', () => skipTime(15));
visualizationMode.addEventListener('click', changeVisualizationMode);
downloadAudio.addEventListener('click', downloadAudioFile);
recordVoiceBtn.addEventListener('click', showRecordModal);
saveDocBtn.addEventListener('click', showSaveModal);
refreshDocsBtn.addEventListener('click', loadSavedDocuments);
searchDocs.addEventListener('input', filterSavedDocuments);
filterDocsBtn.addEventListener('click', showFilterOptions);
sortBtns.forEach(btn => {
btn.addEventListener('click', () => {
const field = btn.dataset.sort;
if (sortField === field) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortField = field;
sortDirection = 'asc';
}
updateSortButtons();
renderSavedDocuments();
});
});
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
renderSavedDocuments();
}
});
nextPageBtn.addEventListener('click', () => {
if (currentPage * docsPerPage < savedDocuments.length) {
currentPage++;
renderSavedDocuments();
}
});
cancelUrlBtn.addEventListener('click', () => urlModal.classList.add('hidden'));
loadUrlBtn.addEventListener('click', loadFromUrl);
cancelSaveBtn.addEventListener('click', () => saveModal.classList.add('hidden'));
confirmSaveBtn.addEventListener('click', saveCurrentDocument);
startRecordBtn.addEventListener('click', startRecording);
stopRecordBtn.addEventListener('click', stopRecording);
playRecordBtn.addEventListener('click', playRecording);
saveRecordBtn.addEventListener('click', saveRecording);
closeRecordBtn.addEventListener('click', () => recordModal.classList.add('hidden'));
// Functions
function updateCounts() {
const text = documentEditor.innerText.trim();
const words = text ? text.split(/\s+/).length : 0;
const chars = text.length;
wordCount.textContent = words;
charCount.textContent = chars;
}
function setupDocumentEditor() {
documentEditor.addEventListener('keydown', (e) => {
// Handle tab key for indentation
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('indent');
}
});
// Show placeholder when empty
documentEditor.addEventListener('focus', () => {
if (documentEditor.innerText === '') {
documentEditor.dataset.placeholder = documentEditor.getAttribute('data-placeholder');
}
});
documentEditor.addEventListener('blur', () => {
if (documentEditor.innerText === '') {
documentEditor.removeAttribute('data-placeholder');
}
});
}
function showDocumentToolbar() {
if (!documentToolbarVisible) {
document.querySelector('.document-toolbar').classList.remove('hidden');
documentToolbarVisible = true;
}
}
function hideDocumentToolbarAfterDelay() {
setTimeout(() => {
if (!documentEditor.contains(document.activeElement)) {
document.querySelector('.document-toolbar').classList.add('hidden');
documentToolbarVisible = false;
}
}, 200);
}
function toggleSpellcheck() {
documentEditor.spellcheck = !documentEditor.spellcheck;
spellcheckBtn.classList.toggle('text-blue-600');
}
function toggleSettingsPanel() {
settingsPanel.classList.toggle('open');
settingsToggle.classList.toggle('text-blue-600');
}
function toggleFullscreen() {
const docContainer = document.querySelector('.document-container');
if (!isFullscreen) {
docContainer.requestFullscreen?.();
docContainer.classList.add('max-h-[80vh]');
fullscreenToggle.innerHTML = '<i class="fas fa-compress"></i>';
} else {
document.exitFullscreen?.();
docContainer.classList.remove('max-h-[80vh]');
fullscreenToggle.innerHTML = '<i class="fas fa-expand"></i>';
}
isFullscreen = !isFullscreen;
}
function switchTab(tab) {
tabBtns.forEach(btn => {
if (btn.dataset.tab === tab) {
btn.classList.add('tab-active');
} else {
btn.classList.remove('tab-active');
}
});
tabContents.forEach(content => {
if (content.id === `${tab}-tab`) {
content.classList.remove('hidden');
} else {
content.classList.add('hidden');
}
});
}
function setupVoiceOptions() {
// Select first voice by default
if (voiceOptions.length > 0) {
voiceOptions[0].classList.add('border-blue-500', 'bg-blue-50');
voiceOptions[0].querySelector('.voice-check').classList.remove('opacity-0');
}
}
function playTTS() {
const text = documentEditor.innerText.trim();
if (!text) return;
if (isPlaying && speechSynthesis.paused) {
speechSynthesis.resume();
playBtn.disabled = true;
pauseBtn.disabled = false;
return;
}
// Stop any ongoing speech
stopTTS();
// Create new utterance
utterance = new SpeechSynthesisUtterance(text);
utterance.voice = speechSynthesis.getVoices().find(v => v.voiceURI === selectedVoice);
utterance.rate = parseFloat(voiceSpeed.value);
utterance.pitch = parseFloat(voicePitch.value) / 10;
// Highlight words as they're spoken
const words = text.split(/\s+/);
let currentWordIndex = 0;
utterance.onboundary = (event) => {
if (event.name === 'word') {
// Remove previous highlight
document.querySelectorAll('.highlight').forEach(el => {
el.classList.remove('highlight');
});
// Find current word position
const charIndex = event.charIndex;
const word = words.find(w => {
const index = text.indexOf(w, charIndex);
return index <= charIndex && index + w.length > charIndex;
});
if (word) {
// Create a temporary element to highlight the word
const temp = document.createElement('span');
temp.className = 'highlight';
temp.textContent = word;
// Replace the word in the textarea (simulated highlighting)
const before = text.substring(0, charIndex);
const after = text.substring(charIndex + word.length);
documentContent.innerHTML = before + temp.outerHTML + after;
}
// Update progress
const progress = (charIndex / text.length) * 100;
progressFill.style.width = `${progress}%`;
// Update current time (simulated)
const duration = text.length / 50; // Approximate duration in seconds
const current = (progress / 100) * duration;
currentTime.textContent = formatTime(current);
totalTime.textContent = formatTime(duration);
}
};
utterance.onstart = () => {
isPlaying = true;
playBtn.disabled = true;
pauseBtn.disabled = false;
stopBtn.disabled = false;
// Show document content for highlighting
documentContent.innerHTML = text;
documentEditor.classList.add('hidden');
documentContent.classList.remove('hidden');
// Animate waveform
animateWaveform();
};
utterance.onend = () => {
isPlaying = false;
playBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = true;
// Reset progress
progressFill.style.width = '0%';
currentTime.textContent = '0:00';
// Stop waveform animation
stopWaveformAnimation();
// Show editor again
documentEditor.classList.remove('hidden');
documentContent.classList.add('hidden');
};
utterance.onerror = (event) => {
console.error('SpeechSynthesis error:', event);
isPlaying = false;
playBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = true;
stopWaveformAnimation();
// Show editor again
documentEditor.classList.remove('hidden');
documentContent.classList.add('hidden');
};
speechSynthesis.speak(utterance);
}
function updateUtteranceSettings() {
if (utterance) {
utterance.rate = parseFloat(voiceSpeed.value);
utterance.pitch = parseFloat(voicePitch.value) / 10;
if (isPlaying) {
speechSynthesis.cancel();
speechSynthesis.speak(utterance);
}
}
}
function pauseTTS() {
if (isPlaying) {
speechSynthesis.pause();
playBtn.disabled = false;
pauseBtn.disabled = true;
stopWaveformAnimation();
}
}
function stopTTS() {
speechSynthesis.cancel();
isPlaying = false;
playBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = true;
progressFill.style.width = '0%';
currentTime.textContent = '0:00';
// Clear highlights
document.querySelectorAll('.highlight').forEach(el => {
el.classList.remove('highlight');
});
documentEditor.classList.remove('hidden');
documentContent.classList.add('hidden');
stopWaveformAnimation();
}
function skipTime(seconds) {
if (!utterance || !isPlaying) return;
// In a real implementation, this would skip the audio playback
// For demo, we'll just show a notification
const direction = seconds > 0 ? 'forward' : 'back';
showNotification(`Skipping ${Math.abs(seconds)} seconds ${direction}`);
}
function clearDocument() {
stopTTS();
documentEditor.innerHTML = '';
documentContent.innerHTML = '';
updateCounts();
}
function changeVisualizationMode() {
// Cycle through different visualization modes
const modes = ['waveform', 'spectrum', 'circle'];
let currentMode = waveform.dataset.mode || 'waveform';
let nextMode = modes[(modes.indexOf(currentMode) + 1) % modes.length];
waveform.dataset.mode = nextMode;
// Change the icon based on mode
const icons = {
'waveform': 'fa-sliders-h',
'spectrum': 'fa-chart-bar',
'circle': 'fa-circle-notch'
};
visualizationMode.innerHTML = `<i class="fas ${icons[nextMode]}"></i>`;
showNotification(`Visualization mode: ${nextMode}`);
}
function downloadAudioFile() {
// In a real implementation, this would download the generated audio
// For demo, we'll show a notification
showNotification('Preparing audio file for download...');
// Simulate download after a delay
setTimeout(() => {
showNotification('Audio download started');
}, 1000);
}
function showRecordModal() {
recordModal.classList.remove('hidden');
recordingName.value = `Recording ${new Date().toLocaleString()}`;
}
function startRecording() {
// Request microphone access
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
isRecording = true;
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = event => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioBlob);
playRecordBtn.disabled = false;
playRecordBtn.onclick = () => {
const audio = new Audio(audioUrl);
audio.play();
};
};
mediaRecorder.start();
startRecordBtn.disabled = true;
stopRecordBtn.disabled = false;
recordStatus.textContent = 'Recording...';
recordStatus.className = 'text-lg font-medium text-red-600';
// Start timer
recordStartTime = Date.now();
recordInterval = setInterval(updateRecordTimer, 1000);
updateRecordTimer();
})
.catch(err => {
console.error('Error accessing microphone:', err);
showNotification('Microphone access denied');
});
}
function updateRecordTimer() {
const elapsed = Math.floor((Date.now() - recordStartTime) / 1000);
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0');
const seconds = (elapsed % 60).toString().padStart(2, '0');
recordTimer.textContent = `${minutes}:${seconds}`;
}
function stopRecording() {
if (!isRecording) return;
isRecording = false;
mediaRecorder.stop();
clearInterval(recordInterval);
// Stop all tracks
mediaRecorder.stream.getTracks().forEach(track => track.stop());
startRecordBtn.disabled = false;
stopRecordBtn.disabled = true;
recordStatus.textContent = 'Recording stopped';
recordStatus.className = 'text-lg font-medium text-green-600';
}
function playRecording() {
// Handled in the onstop event handler
}
function saveRecording() {
const name = recordingName.value.trim() || `Recording ${new Date().toLocaleString()}`;
showNotification(`Recording saved as "${name}"`);
// In a real implementation, this would save to storage
setTimeout(() => {
recordModal.classList.add('hidden');
}, 1000);
}
function loadFromUrl() {
const url = documentUrl.value.trim();
if (!url) return;
// In a real implementation, this would fetch the URL content
// For demo purposes, we'll simulate with a placeholder
documentEditor.innerHTML = `<p>Content from ${url} would be loaded here in a real implementation.</p><p>This demo shows the UI but doesn't actually fetch URL content.</p>`;
updateCounts();
urlModal.classList.add('hidden');
documentUrl.value = '';
}
function showSaveModal() {
const text = documentEditor.innerText.trim();
if (!text) {
showNotification('Please enter some text to save.');
return;
}
documentTitle.value = `Document ${savedDocuments.length + 1}`;
saveModal.classList.remove('hidden');
documentTitle.focus();
}
function saveCurrentDocument() {
const title = documentTitle.value.trim() || `Document ${savedDocuments.length + 1}`;
const text = documentEditor.innerHTML;
const plainText = documentEditor.innerText;
const date = new Date().toLocaleDateString();
const length = plainText.split(/\s+/).length;
// In a real implementation, this would save to Val Town or another backend
savedDocuments.unshift({
title,
text,
plainText,
date,
length,
language: selectedLanguage
});
saveModal.classList.add('hidden');
documentTitle.value = '';
// Update UI
renderSavedDocuments();
currentPage = 1;
// Simulate saving to Val Town
console.log('Document saved to Val Town (simulated):', { title, length });
showNotification(`Document "${title}" saved successfully`);
}
function loadSavedDocuments() {
// In a real implementation, this would load from Val Town
// For demo, we'll use some sample data
if (savedDocuments.length === 0) {
savedDocuments = [
{
title: 'Sample Document 1',
text: '<p>This is a sample document with some text.</p><p>It demonstrates how formatted content would appear.</p>',
plainText: 'This is a sample document with some text.\n\nIt demonstrates how formatted content would appear.',
date: '2023-06-15',
length: 12,
language: 'en-US'
},
{
title: 'Meeting Notes',
text: '<p><strong>Meeting Date:</strong> June 10, 2023</p><p><em>Attendees:</em> John, Sarah, Michael</p><ul><li>Reviewed project timeline</li><li>Discussed budget concerns</li><li>Assigned new tasks</li></ul>',
plainText: 'Meeting Date: June 10, 2023\n\nAttendees: John, Sarah, Michael\n\n- Reviewed project timeline\n- Discussed budget concerns\n- Assigned new tasks',
date: '2023-06-10',
length: 20,
language: 'en-GB'
}
];
}
renderSavedDocuments();
showNotification('Documents refreshed');
}
function filterSavedDocuments() {
const searchTerm = searchDocs.value.toLowerCase();
if (!searchTerm) {
renderSavedDocuments();
return;
}
const filtered = savedDocuments.filter(doc =>
doc.title.toLowerCase().includes(searchTerm) ||
doc.plainText.toLowerCase().includes(searchTerm)
);
renderSavedDocuments(filtered);
}
function showFilterOptions() {
// In a real implementation, this would show filter options
showNotification('Filter options would appear here');
}
function updateSortButtons() {
sortBtns.forEach(btn => {
const field = btn.dataset.sort;
const icon = btn.querySelector('i');
if (field === sortField) {
icon.className = sortDirection === 'asc' ? 'fas fa-sort-up ml-1' : 'fas fa-sort-down ml-1';
} else {
icon.className = 'fas fa-sort ml-1';
}
});
}
function renderSavedDocuments(docs = savedDocuments) {
const sortedDocs = [...docs].sort((a, b) => {
let valA, valB;
if (sortField === 'date') {
valA = new Date(a.date);
valB = new Date(b.date);
} else {
valA = a[sortField];
valB = b[sortField];
}
if (typeof valA === 'string') valA = valA.toLowerCase();
if (typeof valB === 'string') valB = valB.toLowerCase();
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
const startIdx = (currentPage - 1) * docsPerPage;
const paginatedDocs = sortedDocs.slice(startIdx, startIdx + docsPerPage);
if (paginatedDocs.length === 0) {
savedDocsList.innerHTML = `
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500">No documents found</td>
</tr>
`;
} else {
savedDocsList.innerHTML = paginatedDocs.map((doc, idx) => {
const flagClass = doc.language === 'en-US' ? 'flag-us' :
doc.language === 'en-GB' ? 'flag-uk' :
doc.language === 'en-AU' ? 'flag-au' : 'flag-us';
return `
<tr>
<td class="px-6 py-4">
<div class="font-medium text-gray-900">${doc.title}</div>
<div class="text-sm text-gray-500 mt-1 line-clamp-2">${doc.plainText.substring(0, 100)}${doc.plainText.length > 100 ? '...' : ''}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-gray-500">${doc.date}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-gray-500">${doc.length} words</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="language-flag ${flagClass}"></span>
<span class="text-gray-500">${doc.language}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button class="text-blue-600 hover:text-blue-900 mr-3 load-doc-btn" data-index="${startIdx + idx}">
<i class="fas fa-play"></i>
</button>
<button class="text-green-600 hover:text-green-900 mr-3 edit-doc-btn" data-index="${startIdx + idx}">
<i class="fas fa-edit"></i>
</button>
<button class="text-red-600 hover:text-red-900 delete-doc-btn" data-index="${startIdx + idx}">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`;
}).join('');
}
// Update pagination info
showingFrom.textContent = startIdx + 1;
showingTo.textContent = Math.min(startIdx + paginatedDocs.length, sortedDocs.length);
totalDocs.textContent = sortedDocs.length;
// Update pagination buttons
prevPageBtn.disabled = currentPage === 1;
nextPageBtn.disabled = currentPage * docsPerPage >= sortedDocs.length;
// Add event listeners to the new buttons
document.querySelectorAll('.load-doc-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.currentTarget.dataset.index);
loadDocument(index);
});
});
document.querySelectorAll('.edit-doc-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.currentTarget.dataset.index);
editDocument(index);
});
});
document.querySelectorAll('.delete-doc-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.currentTarget.dataset.index);
deleteDocument(index);
});
});
}
function loadDocument(index) {
stopTTS();
const doc = savedDocuments[index];
documentEditor.innerHTML = doc.text;
selectedLanguage = doc.language;
// Update voice selection to match document language
voiceOptions.forEach(option => {
if (option.dataset.lang === doc.language) {
option.click();
}
});
updateCounts();
showNotification(`Loaded document: ${doc.title}`);
}
function editDocument(index) {
// Just load the document for editing
loadDocument(index);
showNotification(`Editing "${savedDocuments[index].title}"`);
}
function deleteDocument(index) {
if (confirm(`Delete "${savedDocuments[index].title}"? This cannot be undone.`)) {
savedDocuments.splice(index, 1);
renderSavedDocuments();
showNotification('Document deleted');
// Adjust current page if needed
const totalPages = Math.ceil(savedDocuments.length / docsPerPage);
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderSavedDocuments();
}
}
}
function animateWaveform() {
const bars = waveform.querySelectorAll('.waveform-bar');
bars.forEach(bar => {
bar.style.animationPlayState = 'running';
});
}
function stopWaveformAnimation() {
const bars = waveform.querySelectorAll('.waveform-bar');
bars.forEach(bar => {
bar.style.animationPlayState = 'paused';
bar.style.height = '10px';
});
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
}
function showNotification(message) {
// In a real app, this would show a nice toast notification
console.log('Notification:', message);
}
// Load voices when they become available
speechSynthesis.onvoiceschanged = function() {
console.log('Voices loaded:', speechSynthesis.getVoices());
// Select the first voice that matches our default
const voices = speechSynthesis.getVoices();
const defaultVoice = voices.find(v => v.voiceURI === selectedVoice);
if (defaultVoice) {
utterance = new SpeechSynthesisUtterance();
utterance.voice = defaultVoice;
}
};
// Try to load voices immediately (might not work in all browsers)
if (speechSynthesis.getVoices().length > 0) {
console.log('Voices already loaded:', speechSynthesis.getVoices());
} else {
console.log('Waiting for voices to load...');
}
// Handle voice selection
voiceOptions.forEach(option => {
option.addEventListener
</html>