Spaces:
Running
Running
| $(document).ready(function() { | |
| // File handling variables | |
| let currentFile = null; | |
| let originalTextContent = null; | |
| let lastUploadedFileName = null; | |
| let fileJustUploaded = false; // Flag to prevent immediate detachment | |
| let currentModelType = window.tokenizerData?.model_type || 'predefined'; | |
| let currentTokenizerInfo = null; | |
| // Try to parse tokenizer info if available from server | |
| try { | |
| currentTokenizerInfo = window.tokenizerData?.tokenizer_info || null; | |
| if (currentTokenizerInfo) { | |
| updateTokenizerInfoDisplay(currentTokenizerInfo, currentModelType === 'custom'); | |
| } | |
| } catch(e) { | |
| console.error("Error parsing tokenizer info:", e); | |
| } | |
| // Show error if exists | |
| if (window.tokenizerData?.error) { | |
| showError(window.tokenizerData.error); | |
| } | |
| // Setup model type based on initial state | |
| if (currentModelType === "custom") { | |
| $('.toggle-option').removeClass('active'); | |
| $('.custom-toggle').addClass('active'); | |
| $('#predefinedModelSelector').hide(); | |
| $('#customModelSelector').show(); | |
| } | |
| // Show success badge if custom model loaded successfully | |
| if (currentModelType === "custom" && !window.tokenizerData?.error) { | |
| $('#modelSuccessBadge').addClass('show'); | |
| setTimeout(() => { | |
| $('#modelSuccessBadge').removeClass('show'); | |
| }, 3000); | |
| } | |
| // Toggle between predefined and custom model inputs | |
| $('.toggle-option').click(function() { | |
| const modelType = $(this).data('type'); | |
| $('.toggle-option').removeClass('active'); | |
| $(this).addClass('active'); | |
| currentModelType = modelType; | |
| if (modelType === 'predefined') { | |
| $('#predefinedModelSelector').show(); | |
| $('#customModelSelector').hide(); | |
| $('#modelTypeInput').val('predefined'); | |
| // Set the model input value to the selected predefined model | |
| $('#modelInput').val($('#modelSelect').val()); | |
| } else { | |
| $('#predefinedModelSelector').hide(); | |
| $('#customModelSelector').show(); | |
| $('#modelTypeInput').val('custom'); | |
| } | |
| // Clear tokenizer info if switching models | |
| if (modelType === 'predefined') { | |
| $('#tokenizerInfoContent').html('<div class="tokenizer-info-loading"><div class="tokenizer-info-spinner"></div></div>'); | |
| fetchTokenizerInfo($('#modelSelect').val(), false); | |
| } else { | |
| $('#customTokenizerInfoContent').html('<div class="tokenizer-info-loading"><div class="tokenizer-info-spinner"></div></div>'); | |
| // Only fetch if there's a custom model value | |
| const customModel = $('#customModelInput').val(); | |
| if (customModel) { | |
| fetchTokenizerInfo(customModel, true); | |
| } | |
| } | |
| }); | |
| // Update hidden input when custom model input changes | |
| $('#customModelInput').on('input', function() { | |
| $('#customModelInputHidden').val($(this).val()); | |
| }); | |
| function showError(message) { | |
| const errorDiv = $('#errorMessage'); | |
| errorDiv.text(message); | |
| errorDiv.show(); | |
| setTimeout(() => errorDiv.fadeOut(), 5000); | |
| } | |
| // Function to update tokenizer info display in tooltip | |
| function updateTokenizerInfoDisplay(info, isCustom = false) { | |
| const targetSelector = isCustom ? '#customTokenizerInfoContent' : '#tokenizerInfoContent'; | |
| let htmlContent = ''; | |
| if (info.error) { | |
| $(targetSelector).html(`<div class="tokenizer-info-error">${info.error}</div>`); | |
| return; | |
| } | |
| // Start building the tooltip content | |
| htmlContent = `<div class="tokenizer-info-header">Tokenizer Details</div> | |
| <div class="tokenizer-info-grid">`; | |
| // Dictionary size | |
| if (info.vocab_size) { | |
| htmlContent += ` | |
| <div class="tokenizer-info-item"> | |
| <span class="tokenizer-info-label">Dictionary Size</span> | |
| <span class="tokenizer-info-value">${info.vocab_size.toLocaleString()}</span> | |
| </div>`; | |
| } | |
| // Tokenizer type | |
| if (info.tokenizer_type) { | |
| htmlContent += ` | |
| <div class="tokenizer-info-item"> | |
| <span class="tokenizer-info-label">Tokenizer Type</span> | |
| <span class="tokenizer-info-value">${info.tokenizer_type}</span> | |
| </div>`; | |
| } | |
| // Max length | |
| if (info.model_max_length) { | |
| htmlContent += ` | |
| <div class="tokenizer-info-item"> | |
| <span class="tokenizer-info-label">Max Length</span> | |
| <span class="tokenizer-info-value">${info.model_max_length.toLocaleString()}</span> | |
| </div>`; | |
| } | |
| htmlContent += `</div>`; // Close tokenizer-info-grid | |
| // Special tokens section | |
| if (info.special_tokens && Object.keys(info.special_tokens).length > 0) { | |
| htmlContent += ` | |
| <div class="tokenizer-info-item" style="margin-top: 0.75rem;"> | |
| <span class="tokenizer-info-label">Special Tokens</span> | |
| <div class="special-tokens-container">`; | |
| // Add each special token with proper escaping for HTML special characters | |
| for (const [tokenName, tokenValue] of Object.entries(info.special_tokens)) { | |
| // Properly escape HTML special characters | |
| const escapedValue = tokenValue | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| htmlContent += ` | |
| <div class="special-token-item"> | |
| <span class="token-name">${tokenName}:</span> | |
| <span class="token-value">${escapedValue}</span> | |
| </div>`; | |
| } | |
| htmlContent += ` | |
| </div> | |
| </div>`; | |
| } | |
| $(targetSelector).html(htmlContent); | |
| } | |
| // Function to show loading overlay | |
| function showLoadingOverlay(text = 'Loading...') { | |
| $('#loadingText').text(text); | |
| $('#loadingOverlay').addClass('active'); | |
| } | |
| // Function to hide loading overlay | |
| function hideLoadingOverlay() { | |
| $('#loadingOverlay').removeClass('active'); | |
| } | |
| // Function to fetch tokenizer info | |
| function fetchTokenizerInfo(modelId, isCustom = false) { | |
| if (!modelId) return; | |
| const targetSelector = isCustom ? '#customTokenizerInfoContent' : '#tokenizerInfoContent'; | |
| $(targetSelector).html('<div class="tokenizer-info-loading"><div class="tokenizer-info-spinner"></div></div>'); | |
| $.ajax({ | |
| url: '/tokenizer-info', | |
| method: 'GET', | |
| data: { | |
| model_id: modelId, | |
| is_custom: isCustom | |
| }, | |
| success: function(response) { | |
| if (response.error) { | |
| $(targetSelector).html(`<div class="tokenizer-info-error">${response.error}</div>`); | |
| } else { | |
| currentTokenizerInfo = response; | |
| updateTokenizerInfoDisplay(response, isCustom); | |
| } | |
| }, | |
| error: function(xhr) { | |
| $(targetSelector).html('<div class="tokenizer-info-error">Failed to load tokenizer information</div>'); | |
| } | |
| }); | |
| } | |
| // Token search functionality | |
| let searchMatches = []; | |
| let currentSearchIndex = -1; | |
| let searchVisible = false; | |
| // Token frequency functionality | |
| let tokenFrequencyData = []; | |
| let showFrequencyChart = false; | |
| function performTokenSearch(searchTerm) { | |
| const tokenContainer = $('#tokenContainer'); | |
| const tokens = tokenContainer.find('.token'); | |
| // Clear previous highlights | |
| tokens.removeClass('highlighted current'); | |
| searchMatches = []; | |
| currentSearchIndex = -1; | |
| if (!searchTerm.trim()) { | |
| updateSearchCount(); | |
| return; | |
| } | |
| const searchLower = searchTerm.toLowerCase(); | |
| // Find matching tokens | |
| tokens.each(function(index) { | |
| const tokenText = $(this).text().toLowerCase(); | |
| if (tokenText.includes(searchLower)) { | |
| $(this).addClass('highlighted'); | |
| searchMatches.push(index); | |
| } | |
| }); | |
| updateSearchCount(); | |
| // Navigate to first match if any | |
| if (searchMatches.length > 0) { | |
| navigateToMatch(0); | |
| } | |
| } | |
| function navigateToMatch(index) { | |
| if (searchMatches.length === 0) return; | |
| // Remove current highlight | |
| $('.token.current').removeClass('current'); | |
| // Update current index | |
| currentSearchIndex = index; | |
| // Highlight current match | |
| const tokenContainer = $('#tokenContainer'); | |
| const tokens = tokenContainer.find('.token'); | |
| const currentToken = tokens.eq(searchMatches[currentSearchIndex]); | |
| currentToken.addClass('current'); | |
| // Scroll to current match - improved logic | |
| const scrollContainer = tokenContainer; | |
| const containerOffset = scrollContainer.offset(); | |
| const tokenOffset = currentToken.offset(); | |
| if (containerOffset && tokenOffset) { | |
| const containerHeight = scrollContainer.height(); | |
| const containerScrollTop = scrollContainer.scrollTop(); | |
| const tokenRelativeTop = tokenOffset.top - containerOffset.top; | |
| // Check if token is outside visible area | |
| if (tokenRelativeTop < 0 || tokenRelativeTop > containerHeight - 50) { | |
| // Calculate new scroll position to center the token | |
| const tokenHeight = currentToken.outerHeight(); | |
| const newScrollTop = containerScrollTop + tokenRelativeTop - (containerHeight / 2) + (tokenHeight / 2); | |
| scrollContainer.animate({ | |
| scrollTop: Math.max(0, newScrollTop) | |
| }, 400, 'swing'); | |
| } | |
| } | |
| updateSearchCount(); | |
| } | |
| function toggleSearchVisibility() { | |
| console.log('toggleSearchVisibility called, current state:', searchVisible); | |
| searchVisible = !searchVisible; | |
| const container = $('#tokenSearchContainer'); | |
| const toggleBtn = $('#searchToggleBtn'); | |
| console.log('Container found:', container.length, 'Toggle button found:', toggleBtn.length); | |
| if (searchVisible) { | |
| // Show the container first, then animate | |
| container.show(); | |
| setTimeout(() => { | |
| container.addClass('show'); | |
| }, 10); | |
| toggleBtn.addClass('active'); | |
| console.log('Showing search container'); | |
| setTimeout(() => { | |
| $('#tokenSearchInput').focus(); | |
| }, 300); | |
| } else { | |
| container.removeClass('show'); | |
| toggleBtn.removeClass('active'); | |
| console.log('Hiding search container'); | |
| setTimeout(() => { | |
| container.hide(); | |
| }, 300); | |
| // Clear search when hiding | |
| $('#tokenSearchInput').val(''); | |
| performTokenSearch(''); | |
| } | |
| } | |
| function updateSearchCount() { | |
| const countText = searchMatches.length > 0 | |
| ? `${currentSearchIndex + 1}/${searchMatches.length}` | |
| : `0/${searchMatches.length}`; | |
| $('#searchCount').text(countText); | |
| // Update navigation button states | |
| $('#prevMatch').prop('disabled', searchMatches.length === 0 || currentSearchIndex <= 0); | |
| $('#nextMatch').prop('disabled', searchMatches.length === 0 || currentSearchIndex >= searchMatches.length - 1); | |
| } | |
| // Token frequency chart functions | |
| function calculateTokenFrequency(tokens) { | |
| const frequencyMap = {}; | |
| tokens.each(function() { | |
| const tokenText = $(this).text(); | |
| if (tokenText.trim()) { | |
| frequencyMap[tokenText] = (frequencyMap[tokenText] || 0) + 1; | |
| } | |
| }); | |
| // Convert to array and sort by frequency | |
| const frequencyArray = Object.entries(frequencyMap) | |
| .map(([token, count]) => ({ token, count })) | |
| .sort((a, b) => b.count - a.count) | |
| .slice(0, 10); // Top 10 tokens | |
| return frequencyArray; | |
| } | |
| function renderFrequencyChart(frequencyData) { | |
| const chartContainer = $('#frequencyChart'); | |
| chartContainer.empty(); | |
| if (frequencyData.length === 0) { | |
| chartContainer.html('<div style="text-align: center; color: var(--secondary-text); padding: 1rem;">No token data available</div>'); | |
| return; | |
| } | |
| const maxCount = frequencyData[0].count; | |
| frequencyData.forEach(({ token, count }) => { | |
| const percentage = (count / maxCount) * 100; | |
| const item = $(` | |
| <div class="frequency-item"> | |
| <div class="frequency-token" data-token="${token}">${token}</div> | |
| <div class="frequency-bar-container"> | |
| <div class="frequency-bar"> | |
| <div class="frequency-bar-fill" style="width: ${percentage}%"></div> | |
| </div> | |
| <div class="frequency-count">${count}</div> | |
| </div> | |
| </div> | |
| `); | |
| // Add click handler to search for this token | |
| item.find('.frequency-token').click(function() { | |
| const searchToken = $(this).data('token'); | |
| $('#tokenSearchInput').val(searchToken); | |
| performTokenSearch(searchToken); | |
| }); | |
| chartContainer.append(item); | |
| }); | |
| } | |
| function toggleFrequencyChart() { | |
| showFrequencyChart = !showFrequencyChart; | |
| const container = $('#frequencyChartContainer'); | |
| const chart = $('#frequencyChart'); | |
| const toggleBtn = $('#toggleFrequencyChart'); | |
| if (showFrequencyChart) { | |
| container.show(); | |
| chart.show(); | |
| toggleBtn.text('Hide Chart').addClass('active'); | |
| // Calculate and render frequency data | |
| const tokens = $('#tokenContainer').find('.token'); | |
| tokenFrequencyData = calculateTokenFrequency(tokens); | |
| renderFrequencyChart(tokenFrequencyData); | |
| } else { | |
| chart.hide(); | |
| toggleBtn.text('Show Chart').removeClass('active'); | |
| } | |
| } | |
| function updateResults(data) { | |
| $('#results').show(); | |
| // Show search toggle button and frequency chart container | |
| $('#searchToggleBtn').show(); | |
| $('#frequencyChartContainer').show(); | |
| // Update tokens | |
| const tokenContainer = $('#tokenContainer'); | |
| tokenContainer.empty(); | |
| data.tokens.forEach(token => { | |
| const span = $('<span>') | |
| .addClass('token') | |
| .css({ | |
| 'background-color': token.colors.background, | |
| 'color': token.colors.text | |
| }) | |
| // Include token id in the tooltip on hover | |
| .attr('title', `Original token: ${token.original} | Token ID: ${token.token_id}`) | |
| .text(token.display); | |
| tokenContainer.append(span); | |
| if (token.newline) { | |
| tokenContainer.append('<br>'); | |
| } | |
| }); | |
| // Re-apply current search if any | |
| const currentSearch = $('#tokenSearchInput').val(); | |
| if (currentSearch.trim()) { | |
| performTokenSearch(currentSearch); | |
| } | |
| // Update display limit notice | |
| if (data.display_limit_reached) { | |
| $('#displayLimitNotice').show(); | |
| $('#totalTokenCount').text(data.total_tokens); | |
| } else { | |
| $('#displayLimitNotice').hide(); | |
| } | |
| // Update preview notice | |
| if (data.preview_only) { | |
| $('#previewNotice').show(); | |
| } else { | |
| $('#previewNotice').hide(); | |
| } | |
| // Update basic stats | |
| $('#totalTokens').text(data.stats.basic_stats.total_tokens); | |
| $('#uniqueTokens').text(`${data.stats.basic_stats.unique_tokens} unique`); | |
| $('#uniquePercentage').text(data.stats.basic_stats.unique_percentage); | |
| $('#specialTokens').text(data.stats.basic_stats.special_tokens); | |
| $('#spaceTokens').text(data.stats.basic_stats.space_tokens); | |
| $('#spaceCount').text(data.stats.basic_stats.space_tokens); | |
| $('#newlineCount').text(data.stats.basic_stats.newline_tokens); | |
| $('#compressionRatio').text(data.stats.basic_stats.compression_ratio); | |
| // Update length stats | |
| $('#avgLength').text(data.stats.length_stats.avg_length); | |
| $('#medianLength').text(data.stats.length_stats.median_length); | |
| $('#stdDev').text(data.stats.length_stats.std_dev); | |
| // Update tokenizer info if available | |
| if (data.tokenizer_info) { | |
| currentTokenizerInfo = data.tokenizer_info; | |
| updateTokenizerInfoDisplay(data.tokenizer_info, currentModelType === 'custom'); | |
| } | |
| } | |
| // Handle text changes to detach file | |
| $('#textInput').on('input', function() { | |
| // Skip if file was just uploaded (prevents immediate detachment) | |
| if (fileJustUploaded) { | |
| fileJustUploaded = false; | |
| return; | |
| } | |
| const currentText = $(this).val(); | |
| const fileInput = document.getElementById('fileInput'); | |
| // Only detach if a file exists and text has been substantially modified | |
| if (fileInput.files.length > 0 && originalTextContent !== null) { | |
| // Check if the text is completely different or has been significantly changed | |
| // This allows for small edits without detaching | |
| const isMajorChange = | |
| currentText.length < originalTextContent.length * 0.8 || // Text reduced by at least 20% | |
| (currentText.length > 0 && | |
| currentText !== originalTextContent.substring(0, currentText.length) && | |
| currentText.substring(0, Math.min(20, currentText.length)) !== | |
| originalTextContent.substring(0, Math.min(20, currentText.length))); | |
| if (isMajorChange) { | |
| detachFile(); | |
| } | |
| } | |
| }); | |
| // Function to detach file | |
| function detachFile() { | |
| // Clear the file input | |
| $('#fileInput').val(''); | |
| // Hide file info | |
| $('#fileInfo').fadeOut(300); | |
| // Reset the original content tracker | |
| originalTextContent = $('#textInput').val(); | |
| // Reset last uploaded filename | |
| lastUploadedFileName = null; | |
| } | |
| // For model changes | |
| $('#modelSelect').change(function() { | |
| const selectedModel = $(this).val(); | |
| $('#modelInput').val(selectedModel); | |
| // Fetch tokenizer info for the selected model | |
| fetchTokenizerInfo(selectedModel, false); | |
| // If text exists, submit the form | |
| if ($('#textInput').val().trim()) { | |
| $('#analyzeForm').submit(); | |
| } | |
| }); | |
| // File drop handling | |
| const fileDropZone = $('#fileDropZone'); | |
| const fileUploadIcon = $('#fileUploadIcon'); | |
| // Prevent default drag behaviors | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| fileDropZone[0].addEventListener(eventName, preventDefaults, false); | |
| document.body.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| // Show drop zone when file is dragged over the document | |
| document.addEventListener('dragenter', showDropZone, false); | |
| document.addEventListener('dragover', showDropZone, false); | |
| fileDropZone[0].addEventListener('dragleave', hideDropZone, false); | |
| fileDropZone[0].addEventListener('drop', hideDropZone, false); | |
| function showDropZone(e) { | |
| fileDropZone.addClass('active'); | |
| } | |
| function hideDropZone() { | |
| fileDropZone.removeClass('active'); | |
| } | |
| // Handle dropped files | |
| fileDropZone[0].addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles(files); | |
| } | |
| // Also handle file selection via click on the icon | |
| fileUploadIcon.on('click', function() { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.onchange = e => { | |
| handleFiles(e.target.files); | |
| }; | |
| input.click(); | |
| }); | |
| function handleFiles(files) { | |
| if (files.length) { | |
| const file = files[0]; | |
| currentFile = file; | |
| lastUploadedFileName = file.name; | |
| fileJustUploaded = true; // Set flag to prevent immediate detachment | |
| // Show file info with animation and add detach button | |
| $('#fileInfo').html(`${file.name} (${formatFileSize(file.size)}) <span class="file-detach" id="fileDetach"><i class="fas fa-times"></i></span>`).fadeIn(300); | |
| // Add click handler for detach button | |
| $('#fileDetach').on('click', function(e) { | |
| e.stopPropagation(); // Prevent event bubbling | |
| detachFile(); | |
| return false; | |
| }); | |
| // Set the file to the file input | |
| const dataTransfer = new DataTransfer(); | |
| dataTransfer.items.add(file); | |
| document.getElementById('fileInput').files = dataTransfer.files; | |
| // Preview in textarea (first 8096 chars) | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const previewText = e.target.result.slice(0, 8096); | |
| $('#textInput').val(previewText); | |
| // Store this as the original content AFTER setting the value | |
| // to prevent the input event from firing and detaching immediately | |
| setTimeout(() => { | |
| originalTextContent = previewText; | |
| // Automatically submit for analysis | |
| $('#analyzeForm').submit(); | |
| }, 50); | |
| }; | |
| reader.readAsText(file); | |
| } | |
| } | |
| function formatFileSize(bytes) { | |
| if (bytes < 1024) return bytes + ' bytes'; | |
| else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; | |
| else return (bytes / 1048576).toFixed(1) + ' MB'; | |
| } | |
| // Make sure to check if there's still a file when analyzing | |
| $('#analyzeForm').on('submit', function(e) { | |
| e.preventDefault(); | |
| // Skip detachment check if file was just uploaded | |
| if (!fileJustUploaded) { | |
| // Check if text has been changed but file is still attached | |
| const textInput = $('#textInput').val(); | |
| const fileInput = document.getElementById('fileInput'); | |
| if (fileInput.files.length > 0 && | |
| originalTextContent !== null && | |
| textInput !== originalTextContent && | |
| textInput.length < originalTextContent.length * 0.8) { | |
| // Text was significantly changed but file is still attached, detach it | |
| detachFile(); | |
| } | |
| } else { | |
| // Reset flag after first submission | |
| fileJustUploaded = false; | |
| } | |
| // Update the hidden inputs based on current model type | |
| if (currentModelType === 'custom') { | |
| $('#customModelInputHidden').val($('#customModelInput').val()); | |
| } else { | |
| $('#modelInput').val($('#modelSelect').val()); | |
| } | |
| const formData = new FormData(this); | |
| const analyzeButton = $('#analyzeButton'); | |
| const originalButtonText = analyzeButton.text(); | |
| analyzeButton.prop('disabled', true); | |
| analyzeButton.html(originalButtonText + '<span class="loading-spinner"></span>'); | |
| showLoadingOverlay('Analyzing text...'); | |
| $.ajax({ | |
| url: '/', | |
| method: 'POST', | |
| data: formData, | |
| processData: false, | |
| contentType: false, | |
| success: function(response) { | |
| if (response.error) { | |
| showError(response.error); | |
| } else { | |
| updateResults(response); | |
| // Show success badge if custom model | |
| if (currentModelType === 'custom') { | |
| $('#modelSuccessBadge').addClass('show'); | |
| setTimeout(() => { | |
| $('#modelSuccessBadge').removeClass('show'); | |
| }, 3000); | |
| } | |
| } | |
| }, | |
| error: function(xhr) { | |
| showError(xhr.responseText || 'An error occurred while processing the text'); | |
| }, | |
| complete: function() { | |
| analyzeButton.prop('disabled', false); | |
| analyzeButton.text(originalButtonText); | |
| hideLoadingOverlay(); | |
| } | |
| }); | |
| }); | |
| $('#expandButton').click(function() { | |
| const container = $('#tokenContainer'); | |
| const isExpanded = container.hasClass('expanded'); | |
| container.toggleClass('expanded'); | |
| $(this).text(isExpanded ? 'Show More' : 'Show Less'); | |
| }); | |
| // Initialize tokenizer info for current model | |
| if (currentModelType === 'predefined') { | |
| fetchTokenizerInfo($('#modelSelect').val(), false); | |
| } else if ($('#customModelInput').val()) { | |
| fetchTokenizerInfo($('#customModelInput').val(), true); | |
| } | |
| // Add event listener for custom model input | |
| $('#customModelInput').on('change', function() { | |
| const modelValue = $(this).val(); | |
| if (modelValue) { | |
| fetchTokenizerInfo(modelValue, true); | |
| } | |
| }); | |
| // Keyboard shortcuts - specifically for textarea | |
| $('#textInput').keydown(function(e) { | |
| // Ctrl+Enter (or Cmd+Enter on Mac) to analyze | |
| if ((e.ctrlKey || e.metaKey) && (e.keyCode === 13 || e.which === 13)) { | |
| e.preventDefault(); | |
| if ($(this).val().trim()) { | |
| $('#analyzeForm').submit(); | |
| } | |
| return false; | |
| } | |
| }); | |
| // Global keyboard shortcuts | |
| $(document).keydown(function(e) { | |
| // Ctrl+F (or Cmd+F on Mac) to toggle search | |
| if ((e.ctrlKey || e.metaKey) && (e.keyCode === 70 || e.which === 70)) { | |
| if ($('#searchToggleBtn').is(':visible')) { | |
| e.preventDefault(); | |
| if (!searchVisible) { | |
| toggleSearchVisibility(); | |
| } else { | |
| $('#tokenSearchInput').focus(); | |
| } | |
| return false; | |
| } | |
| } | |
| // Escape to close search or loading overlay | |
| if (e.keyCode === 27 || e.which === 27) { | |
| if (searchVisible) { | |
| toggleSearchVisibility(); | |
| return false; | |
| } | |
| if ($('#loadingOverlay').hasClass('active')) { | |
| // Don't close if there's an active request | |
| return false; | |
| } | |
| } | |
| }); | |
| // Add keyboard shortcut hint to the textarea placeholder | |
| $('#textInput').attr('placeholder', 'Enter text to analyze or upload a file in bottom left corner... (Ctrl+Enter to analyze)'); | |
| // Token search event handlers | |
| $('#tokenSearchInput').on('input', function() { | |
| const searchTerm = $(this).val(); | |
| performTokenSearch(searchTerm); | |
| }); | |
| $('#nextMatch').click(function() { | |
| if (currentSearchIndex < searchMatches.length - 1) { | |
| navigateToMatch(currentSearchIndex + 1); | |
| } | |
| }); | |
| $('#prevMatch').click(function() { | |
| if (currentSearchIndex > 0) { | |
| navigateToMatch(currentSearchIndex - 1); | |
| } | |
| }); | |
| $('#clearSearch').click(function() { | |
| $('#tokenSearchInput').val(''); | |
| performTokenSearch(''); | |
| }); | |
| // Additional keyboard shortcuts for search | |
| $('#tokenSearchInput').keydown(function(e) { | |
| if (e.keyCode === 13) { // Enter | |
| e.preventDefault(); | |
| if (e.shiftKey) { | |
| // Shift+Enter: previous match | |
| $('#prevMatch').click(); | |
| } else { | |
| // Enter: next match | |
| $('#nextMatch').click(); | |
| } | |
| } else if (e.keyCode === 27) { // Escape | |
| $('#clearSearch').click(); | |
| $(this).blur(); | |
| } | |
| }); | |
| // Search toggle handler using event delegation | |
| $(document).on('click', '#searchToggleBtn', function(e) { | |
| console.log('Search toggle button clicked!'); | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| toggleSearchVisibility(); | |
| return false; | |
| }); | |
| // Frequency chart toggle handler | |
| $('#toggleFrequencyChart').click(function() { | |
| toggleFrequencyChart(); | |
| }); | |
| // Mobile touch enhancements | |
| function addTouchSupport() { | |
| // Add touch-friendly double-tap for expand/collapse | |
| let lastTap = 0; | |
| $('#tokenContainer').on('touchend', function(e) { | |
| const currentTime = new Date().getTime(); | |
| const tapLength = currentTime - lastTap; | |
| if (tapLength < 500 && tapLength > 0) { | |
| $('#expandButton').click(); | |
| e.preventDefault(); | |
| } | |
| lastTap = currentTime; | |
| }); | |
| // Improve touch scrolling for token container | |
| $('#tokenContainer').on('touchstart', function(e) { | |
| this.scrollTop = this.scrollTop; | |
| }); | |
| } | |
| // Check if mobile device and add touch support | |
| if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { | |
| addTouchSupport(); | |
| } | |
| }); |