GitHub Actions commited on
Commit
d730de6
·
1 Parent(s): 7b0064a

Sync from GitHub repo

Browse files
Files changed (1) hide show
  1. templates/arena.html +50 -179
templates/arena.html CHANGED
@@ -28,7 +28,7 @@
28
  <button type="submit" class="mobile-synth-btn">Synthesize</button>
29
  </form>
30
 
31
- <div class="keyboard-hint">
32
  Press <kbd>R</kbd> for random text, <kbd>Enter</kbd> to synthesize, <kbd>N</kbd> for random + synthesize
33
  </div>
34
 
@@ -77,7 +77,7 @@
77
  </div>
78
 
79
  <div class="keyboard-hint">
80
- Press <kbd>Space</kbd> to play/pause audio, <kbd>A</kbd> or <kbd>B</kbd> to vote after listening
81
  </div>
82
  </div>
83
 
@@ -119,7 +119,7 @@
119
  <!-- Script lines will be added here -->
120
  </div>
121
 
122
- <div class="keyboard-hint podcast-keyboard-hint">
123
  Press <kbd>Ctrl</kbd>+<kbd>Enter</kbd> or <kbd>Alt</kbd>+<kbd>Enter</kbd> to add a new line, <kbd>R</kbd> for random script, <kbd>Enter</kbd> to generate, <kbd>N</kbd> for random + generate
124
  </div>
125
 
@@ -171,7 +171,7 @@
171
  </div>
172
 
173
  <div class="keyboard-hint">
174
- Press <kbd>Space</kbd> to play/pause audio, <kbd>A</kbd> or <kbd>B</kbd> to vote after listening
175
  </div>
176
 
177
  <div class="podcast-vote-results vote-results" style="display: none;">
@@ -1011,28 +1011,24 @@
1011
  const rejectedModelNameElement = document.querySelector('.rejected-model-name');
1012
  const modelNameDisplays = document.querySelectorAll('.model-name-display');
1013
  const wavePlayerContainers = document.querySelectorAll('.wave-player-container');
 
1014
 
1015
  let bothSamplesPlayed = false;
1016
  let currentSessionId = null;
1017
  let modelNames = { a: '', b: '' };
1018
  let wavePlayers = { a: null, b: null };
1019
- let cachedSentences = []; // To store sentences available in cache
1020
 
1021
- // Initialize WavePlayers with mobile settings
1022
  wavePlayerContainers.forEach(container => {
1023
  const model = container.dataset.model;
1024
  wavePlayers[model] = new WavePlayer(container, {
1025
- // Add mobile-friendly options but hide native controls
1026
  backend: 'MediaElement',
1027
- mediaControls: false // Hide native audio controls
1028
  });
1029
  });
1030
 
1031
- // Load fallback sentences directly from Flask variable (JSON string)
1032
- // Note: This might cause linter errors, but JSON is JS-compatible.
1033
  const fallbackRandomTexts = JSON.parse({{ harvard_sentences | tojson | safe }});
1034
 
1035
- // Fetch cached sentences on load
1036
  function fetchCachedSentences() {
1037
  fetch('/api/tts/cached-sentences')
1038
  .then(response => response.ok ? response.json() : Promise.reject('Failed to fetch cached sentences'))
@@ -1042,22 +1038,18 @@
1042
  })
1043
  .catch(error => {
1044
  console.error('Error fetching cached sentences:', error);
1045
- // Keep cachedSentences as empty array, fallback will be used
1046
  });
1047
  }
1048
 
1049
- // Check URL hash for direct tab access
1050
  function checkHashAndSetTab() {
1051
  const hash = window.location.hash.toLowerCase();
1052
  if (hash === '#conversational') {
1053
- // Switch to conversational tab
1054
  tabs.forEach(t => t.classList.remove('active'));
1055
  tabContents.forEach(c => c.classList.remove('active'));
1056
 
1057
  document.querySelector('.tab[data-tab="conversational"]').classList.add('active');
1058
  document.getElementById('conversational-tab').classList.add('active');
1059
  } else if (hash === '#tts') {
1060
- // Switch to TTS tab (explicit)
1061
  tabs.forEach(t => t.classList.remove('active'));
1062
  tabContents.forEach(c => c.classList.remove('active'));
1063
 
@@ -1066,29 +1058,22 @@
1066
  }
1067
  }
1068
 
1069
- // Check hash on page load
1070
  checkHashAndSetTab();
1071
 
1072
- // Listen for hash changes
1073
  window.addEventListener('hashchange', checkHashAndSetTab);
1074
 
1075
- // Tab switching functionality
1076
  tabs.forEach(tab => {
1077
  tab.addEventListener('click', function() {
1078
  const tabId = this.dataset.tab;
1079
 
1080
- // Update URL hash without page reload
1081
  history.replaceState(null, null, `#${tabId}`);
1082
 
1083
- // Remove active class from all tabs and contents
1084
  tabs.forEach(t => t.classList.remove('active'));
1085
  tabContents.forEach(c => c.classList.remove('active'));
1086
 
1087
- // Add active class to clicked tab and corresponding content
1088
  this.classList.add('active');
1089
  document.getElementById(`${tabId}-tab`).classList.add('active');
1090
 
1091
- // Reset TTS tab state if switching away from it
1092
  if (tabId !== 'tts') {
1093
  resetToInitialState();
1094
  }
@@ -1113,28 +1098,25 @@
1113
 
1114
  textInput.blur();
1115
 
1116
- // Show loading animation
 
1117
  loadingContainer.style.display = 'flex';
1118
  playersContainer.style.display = 'none';
1119
  voteResultsContainer.style.display = 'none';
1120
  nextRoundContainer.style.display = 'none';
1121
 
1122
- // Reset vote buttons
1123
  voteButtons.forEach(btn => {
1124
  btn.disabled = true;
1125
  btn.classList.remove('selected');
1126
  btn.querySelector('.vote-loader').style.display = 'none';
1127
  });
1128
 
1129
- // Clear model name displays
1130
  modelNameDisplays.forEach(display => {
1131
  display.textContent = '';
1132
  });
1133
 
1134
- // Reset the flag for both samples played
1135
  bothSamplesPlayed = false;
1136
 
1137
- // Call the API to generate TTS
1138
  fetch('/api/tts/generate', {
1139
  method: 'POST',
1140
  headers: {
@@ -1153,25 +1135,19 @@
1153
  .then(data => {
1154
  currentSessionId = data.session_id;
1155
 
1156
- // Load audio in waveplayers
1157
  wavePlayers.a.loadAudio(data.audio_a);
1158
  wavePlayers.b.loadAudio(data.audio_b);
1159
 
1160
- // Show players
1161
  loadingContainer.style.display = 'none';
1162
  playersContainer.style.display = 'flex';
1163
 
1164
- // Setup automatic sequential playback
1165
  wavePlayers.a.wavesurfer.once('ready', function() {
1166
  wavePlayers.a.play();
1167
 
1168
- // When audio A ends, play audio B
1169
  wavePlayers.a.wavesurfer.once('finish', function() {
1170
- // Wait a short moment before playing B
1171
  setTimeout(() => {
1172
  wavePlayers.b.play();
1173
 
1174
- // When audio B ends, enable voting
1175
  wavePlayers.b.wavesurfer.once('finish', function() {
1176
  bothSamplesPlayed = true;
1177
  voteButtons.forEach(btn => {
@@ -1182,7 +1158,6 @@
1182
  });
1183
  });
1184
 
1185
- // Fetch cached sentences again to update the list
1186
  fetchCachedSentences();
1187
  })
1188
  .catch(error => {
@@ -1190,10 +1165,11 @@
1190
  openToast(error.message, "error");
1191
  console.error('Error:', error);
1192
  });
 
 
1193
  }
1194
 
1195
  function handleVote(model) {
1196
- // Disable both vote buttons
1197
  voteButtons.forEach(btn => {
1198
  btn.disabled = true;
1199
  if (btn.dataset.model === model) {
@@ -1201,7 +1177,6 @@
1201
  }
1202
  });
1203
 
1204
- // Send vote to server
1205
  fetch('/api/tts/vote', {
1206
  method: 'POST',
1207
  headers: {
@@ -1221,40 +1196,31 @@
1221
  return response.json();
1222
  })
1223
  .then(data => {
1224
- // Hide loaders
1225
  voteButtons.forEach(btn => {
1226
  btn.querySelector('.vote-loader').style.display = 'none';
1227
 
1228
- // Highlight the selected button
1229
  if (btn.dataset.model === model) {
1230
  btn.classList.add('selected');
1231
  }
1232
  });
1233
 
1234
-
1235
- // Store model names from vote response
1236
  if (data.chosen_model && data.chosen_model.name) {
1237
  modelNames.a = data.names.a;
1238
  modelNames.b = data.names.b;
1239
  }
1240
 
1241
- // Now display model names after voting
1242
  modelNameDisplays[0].textContent = modelNames.a ? `(${modelNames.a})` : '';
1243
  modelNameDisplays[1].textContent = modelNames.b ? `(${modelNames.b})` : '';
1244
 
1245
- // Show vote results
1246
  chosenModelNameElement.textContent = data.chosen_model.name;
1247
  rejectedModelNameElement.textContent = data.rejected_model.name;
1248
  voteResultsContainer.style.display = 'block';
1249
 
1250
- // Show next round button
1251
  nextRoundContainer.style.display = 'block';
1252
 
1253
- // Show success toast
1254
  openToast("Vote recorded successfully!", "success");
1255
  })
1256
  .catch(error => {
1257
- // Re-enable vote buttons
1258
  voteButtons.forEach(btn => {
1259
  btn.disabled = false;
1260
  btn.querySelector('.vote-loader').style.display = 'none';
@@ -1266,56 +1232,45 @@
1266
  }
1267
 
1268
  function resetToInitialState() {
1269
- // Hide players, results, and next round button
1270
  playersContainer.style.display = 'none';
1271
  voteResultsContainer.style.display = 'none';
1272
  nextRoundContainer.style.display = 'none';
1273
 
1274
- // Reset vote buttons
1275
  voteButtons.forEach(btn => {
1276
  btn.disabled = true;
1277
  btn.classList.remove('selected');
1278
  btn.querySelector('.vote-loader').style.display = 'none';
1279
  });
1280
 
1281
- // Clear model name displays
1282
  modelNameDisplays.forEach(display => {
1283
  display.textContent = '';
1284
  });
1285
 
1286
- // Reset model names
1287
  modelNames = { a: '', b: '' };
1288
 
1289
- // Clear text input
1290
  textInput.value = '';
1291
 
1292
- // Stop any playing audio and destroy wavesurfers
1293
  for (const model in wavePlayers) {
1294
  if (wavePlayers[model]) {
1295
  wavePlayers[model].stop();
1296
  }
1297
  }
1298
 
1299
- // Reset session
1300
  currentSessionId = null;
1301
 
1302
- // Reset the flag for both samples played
1303
  bothSamplesPlayed = false;
1304
  }
1305
 
1306
  function handleRandom() {
1307
  let selectedText = '';
1308
  if (cachedSentences && cachedSentences.length > 0) {
1309
- // Select a random text from the cache
1310
  selectedText = cachedSentences[Math.floor(Math.random() * cachedSentences.length)];
1311
  console.log("Using random sentence from cache.");
1312
  } else {
1313
- // Fallback to the initial list if cache is empty or failed to load
1314
  console.log("Cache empty or unavailable, using random sentence from fallback list.");
1315
  if (fallbackRandomTexts && fallbackRandomTexts.length > 0) {
1316
  selectedText = fallbackRandomTexts[Math.floor(Math.random() * fallbackRandomTexts.length)];
1317
  } else {
1318
- // If fallback list is also empty, do nothing. Log an error.
1319
  console.error("Both cached sentences and fallback sentences are unavailable.");
1320
  return;
1321
  }
@@ -1328,10 +1283,8 @@
1328
  openToast("Please listen to both audio samples before voting", "info");
1329
  }
1330
 
1331
- // Add submit event listener to form
1332
  synthForm.addEventListener('submit', handleSynthesize);
1333
 
1334
- // Add click event listeners to vote buttons
1335
  voteButtons.forEach(btn => {
1336
  btn.addEventListener('click', function() {
1337
  if (bothSamplesPlayed) {
@@ -1343,73 +1296,63 @@
1343
  });
1344
  });
1345
 
1346
- // Add keyboard shortcut listeners
1347
  document.addEventListener('keydown', function(e) {
1348
- // Check if TTS tab is active
1349
  const ttsTab = document.getElementById('tts-tab');
1350
  if (!ttsTab.classList.contains('active')) return;
1351
 
1352
- // --- Shortcut logic based on current state ---
1353
  const isInputVisible = playersContainer.style.display === 'none' && loadingContainer.style.display === 'none';
1354
  const isPlaybackVisible = playersContainer.style.display !== 'none';
1355
  const isNextRoundVisible = nextRoundContainer.style.display === 'block';
1356
 
1357
- // Ignore shortcuts if text input is focused
1358
  if (document.activeElement === textInput) {
1359
- // Allow Enter key if focused on text input and input is visible
1360
  if (e.key === 'Enter' && isInputVisible) {
1361
  e.preventDefault();
1362
  handleSynthesize();
1363
  }
1364
- return; // Don't process other shortcuts when input is focused
1365
  }
1366
 
1367
- // Global shortcuts (when not focused on input)
1368
- if (e.key.toLowerCase() === 'r' && isInputVisible) {
1369
- // Only trigger random if not trying to reload (Ctrl+R or Cmd+R)
1370
- if (!e.ctrlKey && !e.metaKey) {
1371
- e.preventDefault();
1372
- handleRandom();
1373
- }
1374
- } else if (e.key === 'Enter' && isInputVisible) {
1375
- e.preventDefault();
1376
- handleSynthesize();
1377
- } else if (e.key.toLowerCase() === 'n' && isInputVisible) {
1378
- // 'N' for New Random Round (only when input is visible)
1379
- if (!e.ctrlKey && !e.metaKey) {
1380
  e.preventDefault();
1381
- handleRandom(); // Get random text
1382
- // Add a slight delay to ensure text input is populated before synthesizing
1383
- setTimeout(() => {
1384
- handleSynthesize(); // Synthesize the random text
1385
- }, 50);
 
 
 
 
1386
  }
1387
  }
1388
 
1389
- // Playback/Voting shortcuts (only when players are visible)
1390
  else if (isPlaybackVisible) {
1391
- if (e.key === ' ') { // Space to play/pause
1392
  e.preventDefault();
1393
- // If A is playing, toggle A, else if B is playing, toggle B, else play A
1394
  if (wavePlayers.a.isPlaying) {
1395
  wavePlayers.a.togglePlayPause();
1396
  } else if (wavePlayers.b.isPlaying) {
1397
  wavePlayers.b.togglePlayPause();
1398
- } else {
1399
- // If neither is playing, prefer playing A if it hasn't finished, else B
1400
  if (wavePlayers.a.wavesurfer.getCurrentTime() < wavePlayers.a.wavesurfer.getDuration()) {
1401
- wavePlayers.a.play();
1402
- } else {
1403
  wavePlayers.b.play();
1404
  }
1405
  }
1406
- } else if (e.key.toLowerCase() === 'a') { // Vote A
1407
  if (bothSamplesPlayed && !voteButtons[0].disabled) {
1408
  handleVote('a');
1409
  } else if (!bothSamplesPlayed) {
1410
  showListenToastMessage();
1411
  }
1412
- } else if (e.key.toLowerCase() === 'b') { // Vote B
1413
  if (bothSamplesPlayed && !voteButtons[1].disabled) {
1414
  handleVote('b');
1415
  } else if (!bothSamplesPlayed) {
@@ -1418,7 +1361,6 @@
1418
  }
1419
  }
1420
 
1421
- // Next Round shortcut (only when next round button is visible)
1422
  else if (isNextRoundVisible && e.key.toLowerCase() === 'n') {
1423
  if (!e.ctrlKey && !e.metaKey) {
1424
  e.preventDefault();
@@ -1427,20 +1369,16 @@
1427
  }
1428
  });
1429
 
1430
- // Add event listener for random button
1431
  randomBtn.addEventListener('click', handleRandom);
1432
 
1433
- // Add event listener for next round button
1434
  nextRoundBtn.addEventListener('click', resetToInitialState);
1435
 
1436
- // Fetch cached sentences when the DOM is ready
1437
  fetchCachedSentences();
1438
  });
1439
  </script>
1440
 
1441
  <script>
1442
  document.addEventListener('DOMContentLoaded', function() {
1443
- // Variables for podcast UI
1444
  const podcastContainer = document.querySelector('.podcast-container');
1445
  const podcastLinesContainer = document.querySelector('.podcast-lines');
1446
  const addLineBtn = document.querySelector('.add-line-btn');
@@ -1456,6 +1394,7 @@
1456
  const podcastNextRoundBtn = podcastPlayerContainer.querySelector('.next-round-btn');
1457
  const chosenModelNameElement = podcastVoteResults.querySelector('.chosen-model-name');
1458
  const rejectedModelNameElement = podcastVoteResults.querySelector('.rejected-model-name');
 
1459
 
1460
  let podcastWavePlayers = { a: null, b: null };
1461
  let bothPodcastSamplesPlayed = false;
@@ -1557,11 +1496,9 @@
1557
  addPodcastLine(2);
1558
  }
1559
 
1560
- // Add a new podcast line
1561
  function addPodcastLine(speakerNum = null) {
1562
  const lineCount = podcastLinesContainer.querySelectorAll('.podcast-line').length;
1563
 
1564
- // If speaker number isn't specified, alternate between 1 and 2
1565
  if (speakerNum === null) {
1566
  speakerNum = (lineCount % 2) + 1;
1567
  }
@@ -1583,10 +1520,8 @@
1583
 
1584
  podcastLinesContainer.appendChild(lineElement);
1585
 
1586
- // Add event listener to remove button
1587
  const removeBtn = lineElement.querySelector('.remove-line-btn');
1588
  removeBtn.addEventListener('click', function() {
1589
- // Don't allow removing if there are only 2 lines
1590
  if (podcastLinesContainer.querySelectorAll('.podcast-line').length > 2) {
1591
  lineElement.remove();
1592
  } else {
@@ -1594,15 +1529,12 @@
1594
  }
1595
  });
1596
 
1597
- // Add event listener for keyboard navigation in the input field
1598
  const inputField = lineElement.querySelector('.line-input');
1599
  inputField.addEventListener('keydown', function(e) {
1600
- // Alt+Enter or Ctrl+Enter to add new line
1601
  if (e.key === 'Enter' && (e.altKey || e.ctrlKey)) {
1602
  e.preventDefault();
1603
  addPodcastLine();
1604
 
1605
- // Focus the new line's input field
1606
  setTimeout(() => {
1607
  const inputs = podcastLinesContainer.querySelectorAll('.line-input');
1608
  inputs[inputs.length - 1].focus();
@@ -1613,24 +1545,18 @@
1613
  return lineElement;
1614
  }
1615
 
1616
- // Load a random script
1617
  function loadRandomScript() {
1618
- // Clear existing lines
1619
  podcastLinesContainer.innerHTML = '';
1620
 
1621
- // Select a random script
1622
  const randomScript = randomScripts[Math.floor(Math.random() * randomScripts.length)];
1623
 
1624
- // Add each line from the script
1625
  randomScript.forEach(line => {
1626
  const lineElement = addPodcastLine(line.speaker);
1627
  lineElement.querySelector('.line-input').value = line.text;
1628
  });
1629
  }
1630
 
1631
- // Generate podcast (mock functionality)
1632
  function generatePodcast() {
1633
- // Get all lines
1634
  const lines = [];
1635
  podcastLinesContainer.querySelectorAll('.podcast-line').forEach(line => {
1636
  const speaker_id = line.querySelector('.speaker-label').textContent.includes('1') ? 0 : 1;
@@ -1641,20 +1567,17 @@
1641
  }
1642
  });
1643
 
1644
- // Validate that we have at least 2 lines with content
1645
  if (lines.length < 2) {
1646
  openToast("Please enter at least 2 lines of dialog", "warning");
1647
  return;
1648
  }
1649
 
1650
- // Reset vote buttons and hide results
1651
  podcastVoteButtons.forEach(btn => {
1652
  btn.disabled = true;
1653
  btn.classList.remove('selected');
1654
  btn.querySelector('.vote-loader').style.display = 'none';
1655
  });
1656
 
1657
- // Clear model name displays
1658
  const modelNameDisplays = podcastPlayerContainer.querySelectorAll('.model-name-display');
1659
  modelNameDisplays.forEach(display => {
1660
  display.textContent = '';
@@ -1663,14 +1586,13 @@
1663
  podcastVoteResults.style.display = 'none';
1664
  podcastNextRoundContainer.style.display = 'none';
1665
 
1666
- // Reset the flag for both samples played
1667
  bothPodcastSamplesPlayed = false;
1668
-
1669
- // Show loading animation
 
1670
  podcastLoadingContainer.style.display = 'flex';
1671
  podcastPlayerContainer.style.display = 'none';
1672
 
1673
- // Call API to generate podcast
1674
  fetch('/api/conversational/generate', {
1675
  method: 'POST',
1676
  headers: {
@@ -1689,30 +1611,23 @@
1689
  .then(data => {
1690
  currentPodcastSessionId = data.session_id;
1691
 
1692
- // Hide loading
1693
  podcastLoadingContainer.style.display = 'none';
1694
 
1695
- // Show player
1696
  podcastPlayerContainer.style.display = 'block';
1697
 
1698
- // Initialize WavePlayers if not already done
1699
  if (!podcastWavePlayers.a) {
1700
  podcastWavePlayers.a = new WavePlayer(podcastWavePlayerA, {
1701
- // Add mobile-friendly options but hide native controls
1702
  backend: 'MediaElement',
1703
- mediaControls: false // Hide native audio controls
1704
  });
1705
  podcastWavePlayers.b = new WavePlayer(podcastWavePlayerB, {
1706
- // Add mobile-friendly options but hide native controls
1707
  backend: 'MediaElement',
1708
- mediaControls: false // Hide native audio controls
1709
  });
1710
 
1711
- // Load audio in waveplayers
1712
  podcastWavePlayers.a.loadAudio(data.audio_a);
1713
  podcastWavePlayers.b.loadAudio(data.audio_b);
1714
 
1715
- // Force hide loading indicators after 5 seconds as a fallback
1716
  setTimeout(() => {
1717
  if (podcastWavePlayers.a && podcastWavePlayers.a.hideLoading) {
1718
  podcastWavePlayers.a.hideLoading();
@@ -1723,19 +1638,16 @@
1723
  console.log('Forced hiding of podcast loading indicators (safety timeout - existing players)');
1724
  }, 5000);
1725
  } else {
1726
- // Reset and reload for existing players
1727
  try {
1728
  podcastWavePlayers.a.wavesurfer.empty();
1729
  podcastWavePlayers.b.wavesurfer.empty();
1730
 
1731
- // Make sure loading indicators are reset
1732
  podcastWavePlayers.a.hideLoading();
1733
  podcastWavePlayers.b.hideLoading();
1734
 
1735
  podcastWavePlayers.a.loadAudio(data.audio_a);
1736
  podcastWavePlayers.b.loadAudio(data.audio_b);
1737
 
1738
- // Force hide loading indicators after 5 seconds as a fallback
1739
  setTimeout(() => {
1740
  if (podcastWavePlayers.a && podcastWavePlayers.a.hideLoading) {
1741
  podcastWavePlayers.a.hideLoading();
@@ -1748,7 +1660,6 @@
1748
  } catch (err) {
1749
  console.error('Error resetting podcast waveplayers:', err);
1750
 
1751
- // Recreate the players if there was an error
1752
  podcastWavePlayers.a = new WavePlayer(podcastWavePlayerA, {
1753
  backend: 'MediaElement',
1754
  mediaControls: false
@@ -1761,7 +1672,6 @@
1761
  podcastWavePlayers.a.loadAudio(data.audio_a);
1762
  podcastWavePlayers.b.loadAudio(data.audio_b);
1763
 
1764
- // Force hide loading indicators after 5 seconds as a fallback
1765
  setTimeout(() => {
1766
  if (podcastWavePlayers.a && podcastWavePlayers.a.hideLoading) {
1767
  podcastWavePlayers.a.hideLoading();
@@ -1774,17 +1684,13 @@
1774
  }
1775
  }
1776
 
1777
- // Setup automatic sequential playback
1778
  podcastWavePlayers.a.wavesurfer.once('ready', function() {
1779
  podcastWavePlayers.a.play();
1780
 
1781
- // When audio A ends, play audio B
1782
  podcastWavePlayers.a.wavesurfer.once('finish', function() {
1783
- // Wait a short moment before playing B
1784
  setTimeout(() => {
1785
  podcastWavePlayers.b.play();
1786
 
1787
- // When audio B ends, enable voting
1788
  podcastWavePlayers.b.wavesurfer.once('finish', function() {
1789
  bothPodcastSamplesPlayed = true;
1790
  podcastVoteButtons.forEach(btn => {
@@ -1800,11 +1706,11 @@
1800
  openToast(error.message, "error");
1801
  console.error('Error:', error);
1802
  });
 
 
1803
  }
1804
 
1805
- // Handle vote for a podcast model
1806
  function handlePodcastVote(model) {
1807
- // Disable both vote buttons
1808
  podcastVoteButtons.forEach(btn => {
1809
  btn.disabled = true;
1810
  if (btn.dataset.model === model) {
@@ -1812,7 +1718,6 @@
1812
  }
1813
  });
1814
 
1815
- // Send vote to server
1816
  fetch('/api/conversational/vote', {
1817
  method: 'POST',
1818
  headers: {
@@ -1832,38 +1737,30 @@
1832
  return response.json();
1833
  })
1834
  .then(data => {
1835
- // Hide loaders
1836
  podcastVoteButtons.forEach(btn => {
1837
  btn.querySelector('.vote-loader').style.display = 'none';
1838
 
1839
- // Highlight the selected button
1840
  if (btn.dataset.model === model) {
1841
  btn.classList.add('selected');
1842
  }
1843
  });
1844
 
1845
- // Store model names from vote response
1846
  podcastModelNames.a = data.names.a;
1847
  podcastModelNames.b = data.names.b;
1848
 
1849
- // Show model names after voting
1850
- const modelNameDisplays = podcastPlayerContainer.querySelectorAll('.model-name-display');
1851
  modelNameDisplays[0].textContent = data.names.a ? `(${data.names.a})` : '';
1852
  modelNameDisplays[1].textContent = data.names.b ? `(${data.names.b})` : '';
1853
 
1854
- // Show vote results
1855
  chosenModelNameElement.textContent = data.chosen_model.name;
1856
  rejectedModelNameElement.textContent = data.rejected_model.name;
1857
  podcastVoteResults.style.display = 'block';
1858
 
1859
- // Show next round button
1860
  podcastNextRoundContainer.style.display = 'block';
1861
 
1862
- // Show success toast
1863
  openToast("Vote recorded successfully!", "success");
1864
  })
1865
  .catch(error => {
1866
- // Re-enable vote buttons
1867
  podcastVoteButtons.forEach(btn => {
1868
  btn.disabled = false;
1869
  btn.querySelector('.vote-loader').style.display = 'none';
@@ -1874,66 +1771,50 @@
1874
  });
1875
  }
1876
 
1877
- // Reset podcast UI to initial state
1878
  function resetPodcastState() {
1879
- // Hide players, results, and next round button
1880
  podcastPlayerContainer.style.display = 'none';
1881
  podcastVoteResults.style.display = 'none';
1882
  podcastNextRoundContainer.style.display = 'none';
1883
 
1884
- // Reset vote buttons
1885
  podcastVoteButtons.forEach(btn => {
1886
  btn.disabled = true;
1887
  btn.classList.remove('selected');
1888
  btn.querySelector('.vote-loader').style.display = 'none';
1889
  });
1890
 
1891
- // Clear model name displays
1892
- const modelNameDisplays = podcastPlayerContainer.querySelectorAll('.model-name-display');
1893
  modelNameDisplays.forEach(display => {
1894
  display.textContent = '';
1895
  });
1896
 
1897
- // Stop any playing audio
1898
  if (podcastWavePlayers.a) podcastWavePlayers.a.stop();
1899
  if (podcastWavePlayers.b) podcastWavePlayers.b.stop();
1900
 
1901
- // Reset session
1902
  currentPodcastSessionId = null;
1903
 
1904
- // Reset the flag for both samples played
1905
  bothPodcastSamplesPlayed = false;
1906
  }
1907
 
1908
- // Add keyboard shortcut listeners for podcast voting
1909
  document.addEventListener('keydown', function(e) {
1910
- // Check if we're in the podcast tab and it's active
1911
  const podcastTab = document.getElementById('conversational-tab');
1912
  if (!podcastTab.classList.contains('active')) return;
1913
 
1914
- // --- Shortcut logic based on current state ---
1915
  const isScriptEditorVisible = podcastPlayerContainer.style.display === 'none' && podcastLoadingContainer.style.display === 'none';
1916
  const isPodcastPlaybackVisible = podcastPlayerContainer.style.display !== 'none';
1917
  const isPodcastNextRoundVisible = podcastNextRoundContainer.style.display === 'block';
1918
 
1919
- // Only process if specific input fields are not focused
1920
  const activeElementTag = document.activeElement.tagName;
1921
  const isInputFocused = activeElementTag === 'INPUT' || activeElementTag === 'TEXTAREA';
1922
 
1923
- // Handle adding new line shortcut separately if input is focused
1924
  if (isInputFocused && e.key === 'Enter' && (e.altKey || e.ctrlKey)) {
1925
- // Already handled by input's keydown listener, prevent double action
1926
  return;
1927
  }
1928
 
1929
- // Ignore other shortcuts if any input/textarea is focused
1930
  if (isInputFocused) {
1931
  return;
1932
  }
1933
 
1934
- // Global shortcuts (when not focused on input)
1935
- if (e.key.toLowerCase() === 'r' && isScriptEditorVisible) {
1936
- // Only trigger random if not trying to reload (Ctrl+R or Cmd+R)
1937
  if (!e.ctrlKey && !e.metaKey) {
1938
  e.preventDefault();
1939
  loadRandomScript();
@@ -1942,41 +1823,36 @@
1942
  e.preventDefault();
1943
  generatePodcast();
1944
  } else if (e.key.toLowerCase() === 'n' && isScriptEditorVisible) {
1945
- // 'N' for New Random Podcast (only when script editor is visible)
1946
  if (!e.ctrlKey && !e.metaKey) {
1947
  e.preventDefault();
1948
- loadRandomScript(); // Get random script
1949
- // Add a slight delay before generating
1950
  setTimeout(() => {
1951
- generatePodcast(); // Generate the random script
1952
  }, 50);
1953
  }
1954
  }
1955
 
1956
- // Playback/Voting shortcuts (only when players are visible)
1957
  else if (isPodcastPlaybackVisible) {
1958
- if (e.key === ' ') { // Space to play/pause
1959
  e.preventDefault();
1960
- // If A is playing, toggle A, else if B is playing, toggle B, else play A
1961
  if (podcastWavePlayers.a && podcastWavePlayers.a.isPlaying) {
1962
  podcastWavePlayers.a.togglePlayPause();
1963
  } else if (podcastWavePlayers.b && podcastWavePlayers.b.isPlaying) {
1964
  podcastWavePlayers.b.togglePlayPause();
1965
  } else if (podcastWavePlayers.a) {
1966
- // If neither is playing, prefer playing A if it hasn't finished, else B
1967
  if (podcastWavePlayers.a.wavesurfer.getCurrentTime() < podcastWavePlayers.a.wavesurfer.getDuration()) {
1968
  podcastWavePlayers.a.play();
1969
  } else if (podcastWavePlayers.b) {
1970
  podcastWavePlayers.b.play();
1971
  }
1972
  }
1973
- } else if (e.key.toLowerCase() === 'a') { // Vote A
1974
  if (bothPodcastSamplesPlayed && !podcastVoteButtons[0].disabled) {
1975
  handlePodcastVote('a');
1976
  } else if (!bothPodcastSamplesPlayed) {
1977
  openToast("Please listen to both audio samples before voting", "info");
1978
  }
1979
- } else if (e.key.toLowerCase() === 'b') { // Vote B
1980
  if (bothPodcastSamplesPlayed && !podcastVoteButtons[1].disabled) {
1981
  handlePodcastVote('b');
1982
  } else if (!bothPodcastSamplesPlayed) {
@@ -1985,7 +1861,6 @@
1985
  }
1986
  }
1987
 
1988
- // Next Round shortcut (only when next round button is visible)
1989
  else if (isPodcastNextRoundVisible && e.key.toLowerCase() === 'n') {
1990
  if (!e.ctrlKey && !e.metaKey) {
1991
  e.preventDefault();
@@ -1994,7 +1869,6 @@
1994
  }
1995
  });
1996
 
1997
- // Event listeners
1998
  addLineBtn.addEventListener('click', function() {
1999
  addPodcastLine();
2000
  });
@@ -2007,7 +1881,6 @@
2007
  generatePodcast();
2008
  });
2009
 
2010
- // Add event listeners to vote buttons
2011
  podcastVoteButtons.forEach(btn => {
2012
  btn.addEventListener('click', function() {
2013
  if (bothPodcastSamplesPlayed) {
@@ -2019,10 +1892,8 @@
2019
  });
2020
  });
2021
 
2022
- // Add event listener for next round button
2023
  podcastNextRoundBtn.addEventListener('click', resetPodcastState);
2024
 
2025
- // Initialize with 2 empty lines
2026
  initializePodcastLines();
2027
  });
2028
  </script>
 
28
  <button type="submit" class="mobile-synth-btn">Synthesize</button>
29
  </form>
30
 
31
+ <div class="keyboard-hint" id="tts-input-hint">
32
  Press <kbd>R</kbd> for random text, <kbd>Enter</kbd> to synthesize, <kbd>N</kbd> for random + synthesize
33
  </div>
34
 
 
77
  </div>
78
 
79
  <div class="keyboard-hint">
80
+ Press <kbd>Space</kbd> to play/pause audio, <kbd>A</kbd> or <kbd>B</kbd> to vote after listening, <kbd>R</kbd> for random text, <kbd>Enter</kbd> to synthesize, <kbd>N</kbd> for new random + synthesize
81
  </div>
82
  </div>
83
 
 
119
  <!-- Script lines will be added here -->
120
  </div>
121
 
122
+ <div class="keyboard-hint podcast-keyboard-hint" id="podcast-input-hint">
123
  Press <kbd>Ctrl</kbd>+<kbd>Enter</kbd> or <kbd>Alt</kbd>+<kbd>Enter</kbd> to add a new line, <kbd>R</kbd> for random script, <kbd>Enter</kbd> to generate, <kbd>N</kbd> for random + generate
124
  </div>
125
 
 
171
  </div>
172
 
173
  <div class="keyboard-hint">
174
+ Press <kbd>Space</kbd> to play/pause audio, <kbd>A</kbd> or <kbd>B</kbd> to vote after listening, <kbd>R</kbd> for random script, <kbd>Enter</kbd> to generate, <kbd>N</kbd> for new random + generate
175
  </div>
176
 
177
  <div class="podcast-vote-results vote-results" style="display: none;">
 
1011
  const rejectedModelNameElement = document.querySelector('.rejected-model-name');
1012
  const modelNameDisplays = document.querySelectorAll('.model-name-display');
1013
  const wavePlayerContainers = document.querySelectorAll('.wave-player-container');
1014
+ const ttsInputHint = document.getElementById('tts-input-hint');
1015
 
1016
  let bothSamplesPlayed = false;
1017
  let currentSessionId = null;
1018
  let modelNames = { a: '', b: '' };
1019
  let wavePlayers = { a: null, b: null };
1020
+ let cachedSentences = [];
1021
 
 
1022
  wavePlayerContainers.forEach(container => {
1023
  const model = container.dataset.model;
1024
  wavePlayers[model] = new WavePlayer(container, {
 
1025
  backend: 'MediaElement',
1026
+ mediaControls: false
1027
  });
1028
  });
1029
 
 
 
1030
  const fallbackRandomTexts = JSON.parse({{ harvard_sentences | tojson | safe }});
1031
 
 
1032
  function fetchCachedSentences() {
1033
  fetch('/api/tts/cached-sentences')
1034
  .then(response => response.ok ? response.json() : Promise.reject('Failed to fetch cached sentences'))
 
1038
  })
1039
  .catch(error => {
1040
  console.error('Error fetching cached sentences:', error);
 
1041
  });
1042
  }
1043
 
 
1044
  function checkHashAndSetTab() {
1045
  const hash = window.location.hash.toLowerCase();
1046
  if (hash === '#conversational') {
 
1047
  tabs.forEach(t => t.classList.remove('active'));
1048
  tabContents.forEach(c => c.classList.remove('active'));
1049
 
1050
  document.querySelector('.tab[data-tab="conversational"]').classList.add('active');
1051
  document.getElementById('conversational-tab').classList.add('active');
1052
  } else if (hash === '#tts') {
 
1053
  tabs.forEach(t => t.classList.remove('active'));
1054
  tabContents.forEach(c => c.classList.remove('active'));
1055
 
 
1058
  }
1059
  }
1060
 
 
1061
  checkHashAndSetTab();
1062
 
 
1063
  window.addEventListener('hashchange', checkHashAndSetTab);
1064
 
 
1065
  tabs.forEach(tab => {
1066
  tab.addEventListener('click', function() {
1067
  const tabId = this.dataset.tab;
1068
 
 
1069
  history.replaceState(null, null, `#${tabId}`);
1070
 
 
1071
  tabs.forEach(t => t.classList.remove('active'));
1072
  tabContents.forEach(c => c.classList.remove('active'));
1073
 
 
1074
  this.classList.add('active');
1075
  document.getElementById(`${tabId}-tab`).classList.add('active');
1076
 
 
1077
  if (tabId !== 'tts') {
1078
  resetToInitialState();
1079
  }
 
1098
 
1099
  textInput.blur();
1100
 
1101
+ if (ttsInputHint) ttsInputHint.style.display = 'none';
1102
+
1103
  loadingContainer.style.display = 'flex';
1104
  playersContainer.style.display = 'none';
1105
  voteResultsContainer.style.display = 'none';
1106
  nextRoundContainer.style.display = 'none';
1107
 
 
1108
  voteButtons.forEach(btn => {
1109
  btn.disabled = true;
1110
  btn.classList.remove('selected');
1111
  btn.querySelector('.vote-loader').style.display = 'none';
1112
  });
1113
 
 
1114
  modelNameDisplays.forEach(display => {
1115
  display.textContent = '';
1116
  });
1117
 
 
1118
  bothSamplesPlayed = false;
1119
 
 
1120
  fetch('/api/tts/generate', {
1121
  method: 'POST',
1122
  headers: {
 
1135
  .then(data => {
1136
  currentSessionId = data.session_id;
1137
 
 
1138
  wavePlayers.a.loadAudio(data.audio_a);
1139
  wavePlayers.b.loadAudio(data.audio_b);
1140
 
 
1141
  loadingContainer.style.display = 'none';
1142
  playersContainer.style.display = 'flex';
1143
 
 
1144
  wavePlayers.a.wavesurfer.once('ready', function() {
1145
  wavePlayers.a.play();
1146
 
 
1147
  wavePlayers.a.wavesurfer.once('finish', function() {
 
1148
  setTimeout(() => {
1149
  wavePlayers.b.play();
1150
 
 
1151
  wavePlayers.b.wavesurfer.once('finish', function() {
1152
  bothSamplesPlayed = true;
1153
  voteButtons.forEach(btn => {
 
1158
  });
1159
  });
1160
 
 
1161
  fetchCachedSentences();
1162
  })
1163
  .catch(error => {
 
1165
  openToast(error.message, "error");
1166
  console.error('Error:', error);
1167
  });
1168
+
1169
+ if (ttsInputHint) ttsInputHint.style.display = 'block';
1170
  }
1171
 
1172
  function handleVote(model) {
 
1173
  voteButtons.forEach(btn => {
1174
  btn.disabled = true;
1175
  if (btn.dataset.model === model) {
 
1177
  }
1178
  });
1179
 
 
1180
  fetch('/api/tts/vote', {
1181
  method: 'POST',
1182
  headers: {
 
1196
  return response.json();
1197
  })
1198
  .then(data => {
 
1199
  voteButtons.forEach(btn => {
1200
  btn.querySelector('.vote-loader').style.display = 'none';
1201
 
 
1202
  if (btn.dataset.model === model) {
1203
  btn.classList.add('selected');
1204
  }
1205
  });
1206
 
 
 
1207
  if (data.chosen_model && data.chosen_model.name) {
1208
  modelNames.a = data.names.a;
1209
  modelNames.b = data.names.b;
1210
  }
1211
 
 
1212
  modelNameDisplays[0].textContent = modelNames.a ? `(${modelNames.a})` : '';
1213
  modelNameDisplays[1].textContent = modelNames.b ? `(${modelNames.b})` : '';
1214
 
 
1215
  chosenModelNameElement.textContent = data.chosen_model.name;
1216
  rejectedModelNameElement.textContent = data.rejected_model.name;
1217
  voteResultsContainer.style.display = 'block';
1218
 
 
1219
  nextRoundContainer.style.display = 'block';
1220
 
 
1221
  openToast("Vote recorded successfully!", "success");
1222
  })
1223
  .catch(error => {
 
1224
  voteButtons.forEach(btn => {
1225
  btn.disabled = false;
1226
  btn.querySelector('.vote-loader').style.display = 'none';
 
1232
  }
1233
 
1234
  function resetToInitialState() {
 
1235
  playersContainer.style.display = 'none';
1236
  voteResultsContainer.style.display = 'none';
1237
  nextRoundContainer.style.display = 'none';
1238
 
 
1239
  voteButtons.forEach(btn => {
1240
  btn.disabled = true;
1241
  btn.classList.remove('selected');
1242
  btn.querySelector('.vote-loader').style.display = 'none';
1243
  });
1244
 
 
1245
  modelNameDisplays.forEach(display => {
1246
  display.textContent = '';
1247
  });
1248
 
 
1249
  modelNames = { a: '', b: '' };
1250
 
 
1251
  textInput.value = '';
1252
 
 
1253
  for (const model in wavePlayers) {
1254
  if (wavePlayers[model]) {
1255
  wavePlayers[model].stop();
1256
  }
1257
  }
1258
 
 
1259
  currentSessionId = null;
1260
 
 
1261
  bothSamplesPlayed = false;
1262
  }
1263
 
1264
  function handleRandom() {
1265
  let selectedText = '';
1266
  if (cachedSentences && cachedSentences.length > 0) {
 
1267
  selectedText = cachedSentences[Math.floor(Math.random() * cachedSentences.length)];
1268
  console.log("Using random sentence from cache.");
1269
  } else {
 
1270
  console.log("Cache empty or unavailable, using random sentence from fallback list.");
1271
  if (fallbackRandomTexts && fallbackRandomTexts.length > 0) {
1272
  selectedText = fallbackRandomTexts[Math.floor(Math.random() * fallbackRandomTexts.length)];
1273
  } else {
 
1274
  console.error("Both cached sentences and fallback sentences are unavailable.");
1275
  return;
1276
  }
 
1283
  openToast("Please listen to both audio samples before voting", "info");
1284
  }
1285
 
 
1286
  synthForm.addEventListener('submit', handleSynthesize);
1287
 
 
1288
  voteButtons.forEach(btn => {
1289
  btn.addEventListener('click', function() {
1290
  if (bothSamplesPlayed) {
 
1296
  });
1297
  });
1298
 
 
1299
  document.addEventListener('keydown', function(e) {
 
1300
  const ttsTab = document.getElementById('tts-tab');
1301
  if (!ttsTab.classList.contains('active')) return;
1302
 
 
1303
  const isInputVisible = playersContainer.style.display === 'none' && loadingContainer.style.display === 'none';
1304
  const isPlaybackVisible = playersContainer.style.display !== 'none';
1305
  const isNextRoundVisible = nextRoundContainer.style.display === 'block';
1306
 
 
1307
  if (document.activeElement === textInput) {
 
1308
  if (e.key === 'Enter' && isInputVisible) {
1309
  e.preventDefault();
1310
  handleSynthesize();
1311
  }
1312
+ return;
1313
  }
1314
 
1315
+ if (isInputVisible) {
1316
+ if (e.key.toLowerCase() === 'r') {
1317
+ if (!e.ctrlKey && !e.metaKey) {
1318
+ e.preventDefault();
1319
+ handleRandom();
1320
+ }
1321
+ } else if (e.key === 'Enter') {
 
 
 
 
 
 
1322
  e.preventDefault();
1323
+ handleSynthesize();
1324
+ } else if (e.key.toLowerCase() === 'n') {
1325
+ if (!e.ctrlKey && !e.metaKey) {
1326
+ e.preventDefault();
1327
+ handleRandom();
1328
+ setTimeout(() => {
1329
+ handleSynthesize();
1330
+ }, 50);
1331
+ }
1332
  }
1333
  }
1334
 
 
1335
  else if (isPlaybackVisible) {
1336
+ if (e.key === ' ') {
1337
  e.preventDefault();
 
1338
  if (wavePlayers.a.isPlaying) {
1339
  wavePlayers.a.togglePlayPause();
1340
  } else if (wavePlayers.b.isPlaying) {
1341
  wavePlayers.b.togglePlayPause();
1342
+ } else if (wavePlayers.a) {
 
1343
  if (wavePlayers.a.wavesurfer.getCurrentTime() < wavePlayers.a.wavesurfer.getDuration()) {
1344
+ wavePlayers.a.play();
1345
+ } else if (wavePlayers.b) {
1346
  wavePlayers.b.play();
1347
  }
1348
  }
1349
+ } else if (e.key.toLowerCase() === 'a') {
1350
  if (bothSamplesPlayed && !voteButtons[0].disabled) {
1351
  handleVote('a');
1352
  } else if (!bothSamplesPlayed) {
1353
  showListenToastMessage();
1354
  }
1355
+ } else if (e.key.toLowerCase() === 'b') {
1356
  if (bothSamplesPlayed && !voteButtons[1].disabled) {
1357
  handleVote('b');
1358
  } else if (!bothSamplesPlayed) {
 
1361
  }
1362
  }
1363
 
 
1364
  else if (isNextRoundVisible && e.key.toLowerCase() === 'n') {
1365
  if (!e.ctrlKey && !e.metaKey) {
1366
  e.preventDefault();
 
1369
  }
1370
  });
1371
 
 
1372
  randomBtn.addEventListener('click', handleRandom);
1373
 
 
1374
  nextRoundBtn.addEventListener('click', resetToInitialState);
1375
 
 
1376
  fetchCachedSentences();
1377
  });
1378
  </script>
1379
 
1380
  <script>
1381
  document.addEventListener('DOMContentLoaded', function() {
 
1382
  const podcastContainer = document.querySelector('.podcast-container');
1383
  const podcastLinesContainer = document.querySelector('.podcast-lines');
1384
  const addLineBtn = document.querySelector('.add-line-btn');
 
1394
  const podcastNextRoundBtn = podcastPlayerContainer.querySelector('.next-round-btn');
1395
  const chosenModelNameElement = podcastVoteResults.querySelector('.chosen-model-name');
1396
  const rejectedModelNameElement = podcastVoteResults.querySelector('.rejected-model-name');
1397
+ const podcastInputHint = document.getElementById('podcast-input-hint');
1398
 
1399
  let podcastWavePlayers = { a: null, b: null };
1400
  let bothPodcastSamplesPlayed = false;
 
1496
  addPodcastLine(2);
1497
  }
1498
 
 
1499
  function addPodcastLine(speakerNum = null) {
1500
  const lineCount = podcastLinesContainer.querySelectorAll('.podcast-line').length;
1501
 
 
1502
  if (speakerNum === null) {
1503
  speakerNum = (lineCount % 2) + 1;
1504
  }
 
1520
 
1521
  podcastLinesContainer.appendChild(lineElement);
1522
 
 
1523
  const removeBtn = lineElement.querySelector('.remove-line-btn');
1524
  removeBtn.addEventListener('click', function() {
 
1525
  if (podcastLinesContainer.querySelectorAll('.podcast-line').length > 2) {
1526
  lineElement.remove();
1527
  } else {
 
1529
  }
1530
  });
1531
 
 
1532
  const inputField = lineElement.querySelector('.line-input');
1533
  inputField.addEventListener('keydown', function(e) {
 
1534
  if (e.key === 'Enter' && (e.altKey || e.ctrlKey)) {
1535
  e.preventDefault();
1536
  addPodcastLine();
1537
 
 
1538
  setTimeout(() => {
1539
  const inputs = podcastLinesContainer.querySelectorAll('.line-input');
1540
  inputs[inputs.length - 1].focus();
 
1545
  return lineElement;
1546
  }
1547
 
 
1548
  function loadRandomScript() {
 
1549
  podcastLinesContainer.innerHTML = '';
1550
 
 
1551
  const randomScript = randomScripts[Math.floor(Math.random() * randomScripts.length)];
1552
 
 
1553
  randomScript.forEach(line => {
1554
  const lineElement = addPodcastLine(line.speaker);
1555
  lineElement.querySelector('.line-input').value = line.text;
1556
  });
1557
  }
1558
 
 
1559
  function generatePodcast() {
 
1560
  const lines = [];
1561
  podcastLinesContainer.querySelectorAll('.podcast-line').forEach(line => {
1562
  const speaker_id = line.querySelector('.speaker-label').textContent.includes('1') ? 0 : 1;
 
1567
  }
1568
  });
1569
 
 
1570
  if (lines.length < 2) {
1571
  openToast("Please enter at least 2 lines of dialog", "warning");
1572
  return;
1573
  }
1574
 
 
1575
  podcastVoteButtons.forEach(btn => {
1576
  btn.disabled = true;
1577
  btn.classList.remove('selected');
1578
  btn.querySelector('.vote-loader').style.display = 'none';
1579
  });
1580
 
 
1581
  const modelNameDisplays = podcastPlayerContainer.querySelectorAll('.model-name-display');
1582
  modelNameDisplays.forEach(display => {
1583
  display.textContent = '';
 
1586
  podcastVoteResults.style.display = 'none';
1587
  podcastNextRoundContainer.style.display = 'none';
1588
 
 
1589
  bothPodcastSamplesPlayed = false;
1590
+
1591
+ if (podcastInputHint) podcastInputHint.style.display = 'none';
1592
+
1593
  podcastLoadingContainer.style.display = 'flex';
1594
  podcastPlayerContainer.style.display = 'none';
1595
 
 
1596
  fetch('/api/conversational/generate', {
1597
  method: 'POST',
1598
  headers: {
 
1611
  .then(data => {
1612
  currentPodcastSessionId = data.session_id;
1613
 
 
1614
  podcastLoadingContainer.style.display = 'none';
1615
 
 
1616
  podcastPlayerContainer.style.display = 'block';
1617
 
 
1618
  if (!podcastWavePlayers.a) {
1619
  podcastWavePlayers.a = new WavePlayer(podcastWavePlayerA, {
 
1620
  backend: 'MediaElement',
1621
+ mediaControls: false
1622
  });
1623
  podcastWavePlayers.b = new WavePlayer(podcastWavePlayerB, {
 
1624
  backend: 'MediaElement',
1625
+ mediaControls: false
1626
  });
1627
 
 
1628
  podcastWavePlayers.a.loadAudio(data.audio_a);
1629
  podcastWavePlayers.b.loadAudio(data.audio_b);
1630
 
 
1631
  setTimeout(() => {
1632
  if (podcastWavePlayers.a && podcastWavePlayers.a.hideLoading) {
1633
  podcastWavePlayers.a.hideLoading();
 
1638
  console.log('Forced hiding of podcast loading indicators (safety timeout - existing players)');
1639
  }, 5000);
1640
  } else {
 
1641
  try {
1642
  podcastWavePlayers.a.wavesurfer.empty();
1643
  podcastWavePlayers.b.wavesurfer.empty();
1644
 
 
1645
  podcastWavePlayers.a.hideLoading();
1646
  podcastWavePlayers.b.hideLoading();
1647
 
1648
  podcastWavePlayers.a.loadAudio(data.audio_a);
1649
  podcastWavePlayers.b.loadAudio(data.audio_b);
1650
 
 
1651
  setTimeout(() => {
1652
  if (podcastWavePlayers.a && podcastWavePlayers.a.hideLoading) {
1653
  podcastWavePlayers.a.hideLoading();
 
1660
  } catch (err) {
1661
  console.error('Error resetting podcast waveplayers:', err);
1662
 
 
1663
  podcastWavePlayers.a = new WavePlayer(podcastWavePlayerA, {
1664
  backend: 'MediaElement',
1665
  mediaControls: false
 
1672
  podcastWavePlayers.a.loadAudio(data.audio_a);
1673
  podcastWavePlayers.b.loadAudio(data.audio_b);
1674
 
 
1675
  setTimeout(() => {
1676
  if (podcastWavePlayers.a && podcastWavePlayers.a.hideLoading) {
1677
  podcastWavePlayers.a.hideLoading();
 
1684
  }
1685
  }
1686
 
 
1687
  podcastWavePlayers.a.wavesurfer.once('ready', function() {
1688
  podcastWavePlayers.a.play();
1689
 
 
1690
  podcastWavePlayers.a.wavesurfer.once('finish', function() {
 
1691
  setTimeout(() => {
1692
  podcastWavePlayers.b.play();
1693
 
 
1694
  podcastWavePlayers.b.wavesurfer.once('finish', function() {
1695
  bothPodcastSamplesPlayed = true;
1696
  podcastVoteButtons.forEach(btn => {
 
1706
  openToast(error.message, "error");
1707
  console.error('Error:', error);
1708
  });
1709
+
1710
+ if (podcastInputHint) podcastInputHint.style.display = 'block';
1711
  }
1712
 
 
1713
  function handlePodcastVote(model) {
 
1714
  podcastVoteButtons.forEach(btn => {
1715
  btn.disabled = true;
1716
  if (btn.dataset.model === model) {
 
1718
  }
1719
  });
1720
 
 
1721
  fetch('/api/conversational/vote', {
1722
  method: 'POST',
1723
  headers: {
 
1737
  return response.json();
1738
  })
1739
  .then(data => {
 
1740
  podcastVoteButtons.forEach(btn => {
1741
  btn.querySelector('.vote-loader').style.display = 'none';
1742
 
 
1743
  if (btn.dataset.model === model) {
1744
  btn.classList.add('selected');
1745
  }
1746
  });
1747
 
 
1748
  podcastModelNames.a = data.names.a;
1749
  podcastModelNames.b = data.names.b;
1750
 
1751
+ modelNameDisplays = podcastPlayerContainer.querySelectorAll('.model-name-display');
 
1752
  modelNameDisplays[0].textContent = data.names.a ? `(${data.names.a})` : '';
1753
  modelNameDisplays[1].textContent = data.names.b ? `(${data.names.b})` : '';
1754
 
 
1755
  chosenModelNameElement.textContent = data.chosen_model.name;
1756
  rejectedModelNameElement.textContent = data.rejected_model.name;
1757
  podcastVoteResults.style.display = 'block';
1758
 
 
1759
  podcastNextRoundContainer.style.display = 'block';
1760
 
 
1761
  openToast("Vote recorded successfully!", "success");
1762
  })
1763
  .catch(error => {
 
1764
  podcastVoteButtons.forEach(btn => {
1765
  btn.disabled = false;
1766
  btn.querySelector('.vote-loader').style.display = 'none';
 
1771
  });
1772
  }
1773
 
 
1774
  function resetPodcastState() {
 
1775
  podcastPlayerContainer.style.display = 'none';
1776
  podcastVoteResults.style.display = 'none';
1777
  podcastNextRoundContainer.style.display = 'none';
1778
 
 
1779
  podcastVoteButtons.forEach(btn => {
1780
  btn.disabled = true;
1781
  btn.classList.remove('selected');
1782
  btn.querySelector('.vote-loader').style.display = 'none';
1783
  });
1784
 
1785
+ modelNameDisplays = podcastPlayerContainer.querySelectorAll('.model-name-display');
 
1786
  modelNameDisplays.forEach(display => {
1787
  display.textContent = '';
1788
  });
1789
 
 
1790
  if (podcastWavePlayers.a) podcastWavePlayers.a.stop();
1791
  if (podcastWavePlayers.b) podcastWavePlayers.b.stop();
1792
 
 
1793
  currentPodcastSessionId = null;
1794
 
 
1795
  bothPodcastSamplesPlayed = false;
1796
  }
1797
 
 
1798
  document.addEventListener('keydown', function(e) {
 
1799
  const podcastTab = document.getElementById('conversational-tab');
1800
  if (!podcastTab.classList.contains('active')) return;
1801
 
 
1802
  const isScriptEditorVisible = podcastPlayerContainer.style.display === 'none' && podcastLoadingContainer.style.display === 'none';
1803
  const isPodcastPlaybackVisible = podcastPlayerContainer.style.display !== 'none';
1804
  const isPodcastNextRoundVisible = podcastNextRoundContainer.style.display === 'block';
1805
 
 
1806
  const activeElementTag = document.activeElement.tagName;
1807
  const isInputFocused = activeElementTag === 'INPUT' || activeElementTag === 'TEXTAREA';
1808
 
 
1809
  if (isInputFocused && e.key === 'Enter' && (e.altKey || e.ctrlKey)) {
 
1810
  return;
1811
  }
1812
 
 
1813
  if (isInputFocused) {
1814
  return;
1815
  }
1816
 
1817
+ if (e.key.toLowerCase() === 'r' && isScriptEditorVisible) {
 
 
1818
  if (!e.ctrlKey && !e.metaKey) {
1819
  e.preventDefault();
1820
  loadRandomScript();
 
1823
  e.preventDefault();
1824
  generatePodcast();
1825
  } else if (e.key.toLowerCase() === 'n' && isScriptEditorVisible) {
 
1826
  if (!e.ctrlKey && !e.metaKey) {
1827
  e.preventDefault();
1828
+ loadRandomScript();
 
1829
  setTimeout(() => {
1830
+ generatePodcast();
1831
  }, 50);
1832
  }
1833
  }
1834
 
 
1835
  else if (isPodcastPlaybackVisible) {
1836
+ if (e.key === ' ') {
1837
  e.preventDefault();
 
1838
  if (podcastWavePlayers.a && podcastWavePlayers.a.isPlaying) {
1839
  podcastWavePlayers.a.togglePlayPause();
1840
  } else if (podcastWavePlayers.b && podcastWavePlayers.b.isPlaying) {
1841
  podcastWavePlayers.b.togglePlayPause();
1842
  } else if (podcastWavePlayers.a) {
 
1843
  if (podcastWavePlayers.a.wavesurfer.getCurrentTime() < podcastWavePlayers.a.wavesurfer.getDuration()) {
1844
  podcastWavePlayers.a.play();
1845
  } else if (podcastWavePlayers.b) {
1846
  podcastWavePlayers.b.play();
1847
  }
1848
  }
1849
+ } else if (e.key.toLowerCase() === 'a') {
1850
  if (bothPodcastSamplesPlayed && !podcastVoteButtons[0].disabled) {
1851
  handlePodcastVote('a');
1852
  } else if (!bothPodcastSamplesPlayed) {
1853
  openToast("Please listen to both audio samples before voting", "info");
1854
  }
1855
+ } else if (e.key.toLowerCase() === 'b') {
1856
  if (bothPodcastSamplesPlayed && !podcastVoteButtons[1].disabled) {
1857
  handlePodcastVote('b');
1858
  } else if (!bothPodcastSamplesPlayed) {
 
1861
  }
1862
  }
1863
 
 
1864
  else if (isPodcastNextRoundVisible && e.key.toLowerCase() === 'n') {
1865
  if (!e.ctrlKey && !e.metaKey) {
1866
  e.preventDefault();
 
1869
  }
1870
  });
1871
 
 
1872
  addLineBtn.addEventListener('click', function() {
1873
  addPodcastLine();
1874
  });
 
1881
  generatePodcast();
1882
  });
1883
 
 
1884
  podcastVoteButtons.forEach(btn => {
1885
  btn.addEventListener('click', function() {
1886
  if (bothPodcastSamplesPlayed) {
 
1892
  });
1893
  });
1894
 
 
1895
  podcastNextRoundBtn.addEventListener('click', resetPodcastState);
1896
 
 
1897
  initializePodcastLines();
1898
  });
1899
  </script>