<%@ page contentType="text/html; charset=UTF-8"%>
<%@ include file="/WEB-INF/jspf/taglibs.jsp"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!-- Version: 2025-10-25-22:30 - Plugin CSS System -->

<div class="center1020">
	<c:set var="tables" value="${frd.tables}"/>

	<shell:title text="AixFlow File Manager"/>
	<shell:state help="plugin/aixflow/index.html#usage-point3"/>

	<h2>${l.l('Upload File')}</h2>
	
	<!-- Drag & Drop Zone -->
	<div id="dropZone" class="drop-zone" style="
		border: 2px dashed #ccc;
		border-radius: 8px;
		padding: 40px;
		text-align: center;
		margin-bottom: 20px;
		background-color: #f9f9f9;
		transition: all 0.3s ease;
		cursor: pointer;
		user-select: none;
	">
		<div id="dropZoneContent">
			<div style="font-size: 48px; margin-bottom: 10px;">📁</div>
			<div style="font-size: 18px; font-weight: bold; margin-bottom: 5px;">Перетащите файлы сюда</div>
			<div style="font-size: 14px; color: #666;">или нажмите для выбора файлов</div>
		</div>
		<div id="dropZoneProgress" style="display: none;">
			<div style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">Загрузка файла...</div>
			<div style="width: 100%; background-color: #e0e0e0; border-radius: 10px; overflow: hidden;">
				<div id="progressBar" style="width: 0%; height: 20px; background-color: #4CAF50; transition: width 0.3s ease;"></div>
			</div>
			<div id="progressText" style="margin-top: 5px; font-size: 14px;">0%</div>
		</div>
	</div>
	
	<form id="uploadForm" action="${form.requestURI}" method="POST" enctype="multipart/form-data" style="display: none;">
		<input type="hidden" name="method" value="uploadFile"/>
		<input type="hidden" name="responseType" value="json"/>
	</form>
	<input type="file" name="file" id="fileInput" multiple accept="audio/*,.mp3,.wav,.m4a,.ogg,.flac" style="display: none;"/>
	
	<button type="button" class="btn-orange mt1" onclick="loadSavedResults()">Load Saved Results</button>
	<button type="button" class="btn-orange mt1" onclick="cleanupN8nResults()" style="margin-left: 5px;">🧹 Cleanup Orphaned Results</button>
	<button type="button" class="btn-orange mt1" onclick="updateDatabaseSchema()" style="margin-left: 5px;">🔧 Update DB Schema</button>
	
	<!-- Bulk Operations Menu -->
	<c:set var="bulkMenuUiid" value="${u:uiid()}"/>
	<c:set var="bulkTableUiid" value="${u:uiid()}"/>
	<c:set var="bulkSelectedUiid" value="${u:uiid()}"/>
	
	<ui:popup-menu id="${bulkMenuUiid}">
		<li><a href="#" onclick="$$.table.select($('#${bulkTableUiid}'), $('#${bulkSelectedUiid}'), 'all'); if (window.updateBulkCounters) updateBulkCounters(); return false;">
			<i class="ti-check-box"></i> ${l.l("Выбрать всё")}
		</a></li>
		<li><a href="#" onclick="$$.table.select($('#${bulkTableUiid}'), $('#${bulkSelectedUiid}'), 'nothing'); if (window.updateBulkCounters) updateBulkCounters(); return false;">
			<i class="ti-control-stop"></i> ${l.l("Очистить выделение")}
		</a></li>
		<li><a href="#" onclick="$$.table.select($('#${bulkTableUiid}'), $('#${bulkSelectedUiid}'), 'invert'); if (window.updateBulkCounters) updateBulkCounters(); return false;">
			<i class="ti-control-shuffle"></i> ${l.l("Инвертировать выделение")}
		</a></li>
		<li><a href="#" onclick="bulkSendToN8n(); return false;">
			<i class="ti-rocket"></i> ${l.l("🚀 Send to AIXFLOW")} [<span data-counter="${bulkSelectedUiid}">0</span>]
		</a></li>
		<li><a href="#" onclick="bulkSendDiarization(); return false;">
			<i class="ti-user"></i> ${l.l("👥 Diarize Texts")} [<span data-counter="${bulkSelectedUiid}">0</span>]
		</a></li>
 		<!-- duplicate items removed: use single counter via data-counter -->
		<li><a href="#" onclick="bulkCheckStatus(); return false;">
			<i class="ti-refresh"></i> ${l.l("📊 Check Status")} [<span data-counter="${bulkSelectedUiid}">0</span>]
		</a></li>
		<li><a href="#" onclick="
			if (!($('#${bulkSelectedUiid}').text() > 0) || !confirm('Удалить выбранные?'))
				return false;
			bulkDeleteFiles();
			return false;
		">
			<i class="ti-trash"></i> ${l.l("Удалить выбранные")} [<span data-counter="${bulkSelectedUiid}">0</span>]
		</a></li>
	</ui:popup-menu>

	<!-- Hidden counter holder for bulk selection; used by $$.table.select -->
	<span id="${bulkSelectedUiid}" style="display:none">0</span>

	<h2>${l.l('Files')}</h2>

	<c:set var="files" value="<%=org.bgerp.plugin.ai.aixflow.action.AixFlowActionPoint3.FILE_AIXFLOW%>"/>

	<c:set var="deletionAllowed" value="${ctxUser.checkPerm(files.deletePermissionAction)}"/>

	<form action="${files.deleteURL}">
		<table class="data hl" id="${bulkTableUiid}">
			<tr>
				<td width="1em">
					<button type="button" class="btn-white icon btn-small" onclick="$$.ui.menuInit($(this), $('#${bulkMenuUiid}'), 'left', true);" title="${l.l('Больше')}">
						<i class="ti-more"></i>
					</button>
				</td>
				<td width="1em"></td>
				<td width="40%">${l.l('File')}</td>
				<td width="30%">${l.l('Modification time')}</td>
				<td>${l.l('Size')}</td>
			</tr>
			<c:forEach var="file" items="${files.list()}">
				<tr class="file-row" data-filename="${file.name}">
					<td style="text-align: center;">
						<input type="checkbox" name="selectedFiles" value="${file.name}"/>
					</td>
					<td style="text-align: center;">
						<button type="button" class="btn-white btn-small icon" onclick="toggleFileDetails('${file.name}')" title="${l.l('Show/Hide Details')}">
							<i class="ti-angle-down" id="icon-${file.name}"></i>
						</button>
					</td>
					<td>
						<c:set var="a">
							<c:if test="${ctxUser.checkPerm(files.downloadPermissionAction)}">
								<c:url var="url" value="${files.downloadURL}">
									<c:param name="name">${file.name}</c:param>
								</c:url>
								<a href="${url}">
							</c:if>
						</c:set>
						${a}
						${file.name}
						<c:if test="${not empty a}"></a></c:if>
					</td>
					<td>${tu.format(tu.convertLongToTimestamp(file.lastModified()), 'ymdhms')}</td>
					<td nowrap>${fu.byteCountToDisplaySize(file.length())}</td>
				</tr>
				<tr class="file-details" id="details-${file.name}" style="display: none;">
					<td colspan="5">
						<div class="file-info-panel" style="background-color: #f8f9fa; padding: 10px; border: 1px solid #dee2e6; border-radius: 4px; margin: 5px 0;">
							<h4>${l.l('File Information')}</h4>
							<div style="display: flex; gap: 10px; flex-wrap: wrap;">
								<div><strong>${l.l('Name')}:</strong> ${file.name}</div>
								<div><strong>${l.l('Size')}:</strong> ${fu.byteCountToDisplaySize(file.length())}</div>
								<div><strong>${l.l('Modified')}:</strong> ${tu.format(tu.convertLongToTimestamp(file.lastModified()), 'ymdhms')}</div>
								<div><strong>${l.l('Type')}:</strong> 
									<c:set var="fileName" value="${file.name}"/>
									<c:set var="fileExtension" value="${fn:toLowerCase(fn:substringAfter(fileName, '.'))}"/>
									<c:choose>
										<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.mp3') or fn:endsWith(fn:toLowerCase(fileName), '.wav') or fn:endsWith(fn:toLowerCase(fileName), '.ogg') or fn:endsWith(fn:toLowerCase(fileName), '.flac') or fn:endsWith(fn:toLowerCase(fileName), '.aac') or fn:endsWith(fn:toLowerCase(fileName), '.m4a')}">
											🔊 ${l.l('Audio')}
										</c:when>
										<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.mp4') or fn:endsWith(fn:toLowerCase(fileName), '.webm') or fn:endsWith(fn:toLowerCase(fileName), '.avi') or fn:endsWith(fn:toLowerCase(fileName), '.mov') or fn:endsWith(fn:toLowerCase(fileName), '.wmv') or fn:endsWith(fn:toLowerCase(fileName), '.flv') or fn:endsWith(fn:toLowerCase(fileName), '.mkv')}">
											🎬 ${l.l('Video')}
										</c:when>
										<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.jpg') or fn:endsWith(fn:toLowerCase(fileName), '.jpeg') or fn:endsWith(fn:toLowerCase(fileName), '.png') or fn:endsWith(fn:toLowerCase(fileName), '.gif') or fn:endsWith(fn:toLowerCase(fileName), '.bmp') or fn:endsWith(fn:toLowerCase(fileName), '.svg') or fn:endsWith(fn:toLowerCase(fileName), '.webp')}">
											🖼️ ${l.l('Image')}
										</c:when>
										<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.pdf')}">
											📄 ${l.l('PDF')}
										</c:when>
										<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.txt') or fn:endsWith(fn:toLowerCase(fileName), '.csv') or fn:endsWith(fn:toLowerCase(fileName), '.md') or fn:endsWith(fn:toLowerCase(fileName), '.html') or fn:endsWith(fn:toLowerCase(fileName), '.htm') or fn:endsWith(fn:toLowerCase(fileName), '.xml') or fn:endsWith(fn:toLowerCase(fileName), '.json')}">
											📝 ${l.l('Text')}
										</c:when>
										<c:otherwise>
											📁 ${l.l('File')} (${fileExtension})
										</c:otherwise>
									</c:choose>
								</div>
							</div>
							<div style="margin-top: 10px;">
								<c:set var="fileName" value="${file.name}"/>
								<c:choose>
									<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.mp3') or fn:endsWith(fn:toLowerCase(fileName), '.wav') or fn:endsWith(fn:toLowerCase(fileName), '.ogg') or fn:endsWith(fn:toLowerCase(fileName), '.flac') or fn:endsWith(fn:toLowerCase(fileName), '.aac') or fn:endsWith(fn:toLowerCase(fileName), '.m4a')}">
										<c:url var="downloadUrl" value="${files.downloadURL}">
											<c:param name="name">${file.name}</c:param>
										</c:url>
										<button type="button" class="btn-blue" onclick="playAudio('${downloadUrl}', '${file.name}')">
											🔊 ${l.l('Play Audio')}
										</button>
									</c:when>
									<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.mp4') or fn:endsWith(fn:toLowerCase(fileName), '.webm') or fn:endsWith(fn:toLowerCase(fileName), '.avi') or fn:endsWith(fn:toLowerCase(fileName), '.mov') or fn:endsWith(fn:toLowerCase(fileName), '.wmv') or fn:endsWith(fn:toLowerCase(fileName), '.flv') or fn:endsWith(fn:toLowerCase(fileName), '.mkv')}">
										<c:url var="downloadUrl" value="${files.downloadURL}">
											<c:param name="name">${file.name}</c:param>
										</c:url>
										<button type="button" class="btn-blue" onclick="playVideo('${downloadUrl}', '${file.name}')">
											🎬 ${l.l('Play Video')}
										</button>
									</c:when>
									<c:when test="${fn:endsWith(fn:toLowerCase(fileName), '.jpg') or fn:endsWith(fn:toLowerCase(fileName), '.jpeg') or fn:endsWith(fn:toLowerCase(fileName), '.png') or fn:endsWith(fn:toLowerCase(fileName), '.gif') or fn:endsWith(fn:toLowerCase(fileName), '.bmp') or fn:endsWith(fn:toLowerCase(fileName), '.svg') or fn:endsWith(fn:toLowerCase(fileName), '.webp')}">
										<c:url var="downloadUrl" value="${files.downloadURL}">
											<c:param name="name">${file.name}</c:param>
										</c:url>
										<button type="button" class="btn-blue" onclick="viewImage('${downloadUrl}', '${file.name}')">
											🖼️ ${l.l('View Image')}
										</button>
									</c:when>
								</c:choose>
								<c:if test="${ctxUser.checkPerm(files.downloadPermissionAction)}">
									<c:url var="downloadUrl" value="${files.downloadURL}">
										<c:param name="name">${file.name}</c:param>
									</c:url>
									<a href="${downloadUrl}" class="btn-grey" style="text-decoration: none; display: inline-block; padding: 5px 10px; margin-left: 5px;">
										⬇️ ${l.l('Download')}
									</a>
								</c:if>
							<button type="button" class="btn-blue" onclick="sendFileToAIXFLOW('${file.name}')" style="margin-left: 5px;">
								🚀 ${l.l('Send to AIXFLOW')}
								</button>
							</div>
						</div>
					</td>
				</tr>
			</c:forEach>
		</table>
	</form>

	<script>
		// Version: 2024-10-13 Direct N8n Send with DB Storage + Drag & Drop
		var savedResults = {};
		var textDataCache = {}; // Cache for text data in analytics panel
		
		// Initialize Drag & Drop
		if (document.readyState === 'loading') {
			document.addEventListener('DOMContentLoaded', function() {
				console.log('DOMContentLoaded fired');
				initializeDragAndDrop();
				loadSavedResults();
				checkForNewlyUploadedFile();
				initializeBulkOperations();
			});
		} else {
			console.log('DOM already ready');
			initializeDragAndDrop();
			loadSavedResults();
			checkForNewlyUploadedFile();
			initializeBulkOperations();
		}
		
		function initializeBulkOperations() {
			// Initialize table selection functionality
			$$.table.select($('#${bulkTableUiid}'), $('#${bulkSelectedUiid}'), 'init');
			
			// Apply status highlighting to file rows
			applyStatusHighlighting();

			// Keep counters in popup menu synced for all actions
			window.updateBulkCounters = function(){
				var cnt = $('#${bulkTableUiid}').find('input[type="checkbox"]:checked').length;
				$('#${bulkSelectedUiid}').text(cnt);
				$('[data-counter="${bulkSelectedUiid}"]').each(function(){ this.textContent = cnt; });
			};
			window.updateBulkCounters();
			// observe checkbox attribute changes in table, not the counter node (to avoid loops)
			var tableNode = document.getElementById('${bulkTableUiid}');
			if (tableNode) {
				var observer = new MutationObserver(function(){ window.updateBulkCounters(); });
				observer.observe(tableNode, { attributes: true, subtree: true, attributeFilter: ['checked'] });
			}

			// also update counters on checkbox clicks as a fallback
			$('#${bulkTableUiid}').on('change', 'input[name="selectedFiles"]', window.updateBulkCounters);

			// Load list of files from kernel storage via mapping
			loadAixflowFiles();
		}

		function loadAixflowFiles() {
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
				body: 'method=getAixflowFiles&responseType=json'
			})
			.then(r => r.json())
			.then(resp => {
				var data = resp.data || resp;
				if (!data.success) return;
				var tbody = document.querySelector('#${bulkTableUiid}');
				// Keep header, rebuild body rows
				var header = tbody.querySelector('tr');
				var parent = tbody.parentNode;
				var table = tbody;
				// remove all rows except header
				while (table.rows.length > 1) table.deleteRow(1);
				
				(data.files || []).forEach(function(f){
					var tr = document.createElement('tr');
					tr.className = 'file-row';
					tr.setAttribute('data-filename', f.filename);
					tr.setAttribute('data-fileid', f.fileId);
					tr.setAttribute('data-secret', f.secret);
					tr.setAttribute('data-title', f.title);
					tr.innerHTML =
						'<td style="text-align:center;"><input type="checkbox" name="selectedFiles" value="'+f.fileId+'"/></td>'+
						'<td style="text-align:center;"><button type="button" class="btn-white btn-small icon" onclick="toggleFileDetails(\''+f.filename+'\')" title="Show/Hide Details"><i class="ti-angle-down" id="icon-'+f.filename+'"></i></button></td>'+
						'<td><a href="/user/file.do?id='+f.fileId+'&title='+encodeURIComponent(f.title)+'&secret='+f.secret+'">'+f.title+'</a></td>'+
						'<td>'+new Date(f.dt).toLocaleString()+'</td>'+
						'<td nowrap>' + (f.size ? humanFileSize(f.size) : '') + '</td>';
					table.appendChild(tr);

					// Append hidden details row with action buttons
					var detailsTr = document.createElement('tr');
					detailsTr.className = 'file-details';
					detailsTr.id = 'details-' + f.filename;
					detailsTr.style.display = 'none';
					var td = document.createElement('td');
					td.colSpan = 5;
					td.innerHTML =
						'<div class="file-info-panel" style="background-color:#f8f9fa;padding:10px;border:1px solid #dee2e6;border-radius:4px;margin:5px 0;">'+
						  '<h4>File Information</h4>'+
						  '<div style="display:flex;gap:10px;flex-wrap:wrap;">'+
						    '<div><strong>Name:</strong> '+f.filename+'</div>'+
						    '<div><strong>Modified:</strong> '+new Date(f.dt).toLocaleString()+'</div>'+
						  '</div>'+
						  '<div style="margin-top:10px;">'+
						    '<button type="button" class="btn-blue" onclick="playAudio(\'/user/file.do?id='+f.fileId+'&title='+encodeURIComponent(f.title)+'&secret='+f.secret+'\', \''+f.filename+'\')">🔊 Play Audio</button>'+
						    '<a href="/user/file.do?id='+f.fileId+'&title='+encodeURIComponent(f.title)+'&secret='+f.secret+'" class="btn-grey" style="text-decoration:none;display:inline-block;padding:5px 10px;margin-left:5px;">⬇️ Download</a>'+
						    '<button type="button" class="btn-blue" onclick="sendFileToAIXFLOW(\''+f.filename+'\')" style="margin-left: 5px;">🚀 Send to AIXFLOW</button>'+
						  '</div>'+
						'</div>';
					detailsTr.appendChild(td);
					table.appendChild(detailsTr);
				});
				applyStatusHighlighting();
				
				// Re-initialize table selection functionality after loading files
				$$.table.select($('#${bulkTableUiid}'), $('#${bulkSelectedUiid}'), 'init');
				
				// Re-attach checkbox change handler
				$('#${bulkTableUiid}').off('change', 'input[name="selectedFiles"]').on('change', 'input[name="selectedFiles"]', window.updateBulkCounters);
			});
		}
		
		function applyStatusHighlighting() {
			// Remove existing status classes
			$('.file-row').removeClass('status-completed status-error status-processing status-queued has-analytics has-diarization');
			
			// Apply status classes based on saved results
			for (var filename in savedResults) {
				var result = savedResults[filename];
				var fileRow = $('.file-row[data-filename="' + filename + '"]');
				
				if (fileRow.length > 0) {
					// Apply status class
					if (result.status === 'completed') {
						fileRow.addClass('status-completed');
					} else if (result.status === 'error') {
						fileRow.addClass('status-error');
					} else if (result.status === 'processing') {
						fileRow.addClass('status-processing');
					} else if (result.status === 'queued') {
						fileRow.addClass('status-queued');
					}
					
					// Visual flags: transcription vs diarization
					var hasText = result.text && result.text.trim() !== '' && result.text !== 'null';
					var hasDiarization = result.diarizationResult && result.diarizationResult.trim() !== '' && result.diarizationResult !== 'null';
					if (hasDiarization) {
						fileRow.addClass('has-diarization');
					} else if (hasText) {
						fileRow.addClass('has-analytics');
					}
				}
			}
		}

		function humanFileSize(bytes) {
			if (!bytes || bytes <= 0) return '0 B';
			var i = Math.floor(Math.log(bytes) / Math.log(1024));
			var sizes = ['B','KB','MB','GB','TB'];
			return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
		}
		
		function generateTaskId() {
			return 'bulk-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
		}
		
		function bulkSendToN8n() {
			var selectedFiles = getSelectedFiles();
			if (selectedFiles.length === 0) {
				alert('Выберите файлы для отправки в n8n!');
				return;
			}
			
			if (!confirm('Отправить ' + selectedFiles.length + ' файлов в n8n?')) {
				return;
			}
			
			console.log('Bulk sending to n8n:', selectedFiles);
			
			// Send files one by one without individual confirmations
			var completed = 0;
			var total = selectedFiles.length;
			
			selectedFiles.forEach(function(fileId, index) {
				setTimeout(function() {
					sendFileToN8nBulk(fileId);
					completed++;
					
					if (completed === total) {
						console.log('All files sent to n8n');
						// Reload page after all files are sent
						setTimeout(function() {
							$$.ajax.loadContent('${form.requestUrl}', this);
						}, 2000);
					}
				}, index * 1000); // 1 second delay between files
			});
		}

		// Bulk diarization: send saved transcription texts for selected files
		function bulkSendDiarization() {
			var selectedFiles = getSelectedFiles();
			if (selectedFiles.length === 0) {
				alert('${l.l("Выберите файлы для отправки на диаризацию!")}');
				return;
			}
			if (!confirm('${l.l("Отправить тексты выбранных файлов на диаризацию?")} (' + selectedFiles.length + ')')) {
				return;
			}
			var completed = 0, total = selectedFiles.length;
			selectedFiles.forEach(function(fileId, index) {
				setTimeout(function(){
					var row = document.querySelector('[data-fileid="' + fileId + '"]') || document.querySelector('tr input[name="selectedFiles"][value="'+fileId+'"]').closest('tr');
					if (!row) { completed++; return; }
					var filename = row.getAttribute('data-filename');
					var cache = textDataCache[filename] || {}; var saved = savedResults[filename] || {};
					var text = cache.text || saved.text || '';
					var hash = saved.hash || '';
					if (!text || text === 'null') { completed++; return; }
					sendTextForDiarization(filename, text, hash);
					completed++;
					if (completed === total) {
						setTimeout(function(){ loadSavedResults(); }, 1500);
					}
				}, index * 400);
			});
		}
		
		function sendFileToN8nBulk(fileId) {
			if (!fileId) {
				console.error('Filename is empty!');
				return;
			}
			
			console.log('Sending file to n8n (bulk, fileId):', fileId);
			
			// Get n8n config first
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
				body: 'method=getN8nConfig&responseType=json'
			})
			.then(response => response.json())
			.then(configData => {
				var config = configData.data || configData;
				if (!config.success) {
					console.error('Failed to get n8n config:', config.message);
					return;
				}
				
				// Read file via /user/file.do using attributes from the row
				var row = document.querySelector('[data-fileid="' + fileId + '"]') || document.querySelector('tr input[name="selectedFiles"][value="'+fileId+'"]').closest('tr');
				if (!row) { console.error('Row not found for fileId', fileId); return; }
				var title = row.getAttribute('data-title');
				var secret = row.getAttribute('data-secret');
				var fileUrl = '/user/file.do?id=' + encodeURIComponent(fileId) + '&title=' + encodeURIComponent(title) + '&secret=' + encodeURIComponent(secret);
				
				fetch(fileUrl)
				.then(response => response.blob())
				.then(blob => {
					// Convert to base64
					var reader = new FileReader();
					reader.onload = function() {
						var base64Data = reader.result.split(',')[1];
						
						// Send to n8n
						var n8nPayload = {
							fileSize: blob.size,
							taskId: generateTaskId(),
							fileContent: base64Data
						};
						
						var auth = btoa(config.n8nAuthUsername + ':' + config.n8nAuthPassword);
						
						fetch(config.n8nUrl, {
							method: 'POST',
							headers: {
								'Content-Type': 'application/json',
								'Authorization': 'Basic ' + auth
							},
							body: JSON.stringify(n8nPayload)
						})
						.then(response => response.json())
						.then(responseData => {
						console.log('N8n response for fileId', fileId, ':', responseData);
							
							// Save result to database
							if (responseData.data && responseData.data.hash) {
								saveN8nResultBulk(title, responseData);
							}
						})
						.catch(error => {
							console.error('Error sending to n8n:', error);
						});
					};
					reader.readAsDataURL(blob);
				})
				.catch(error => {
					console.error('Error reading file:', error);
				});
			})
			.catch(error => {
				console.error('Error getting n8n config:', error);
			});
		}
		
		function saveN8nResultBulk(filename, responseData) {
			var data = responseData.data || {};
			
			var saveData = {
				method: 'saveN8nResult',
				responseType: 'json',
				filename: filename,
				hash: data.hash || '',
				status: data.status || 'processing',
				text: data.text || '',
				durationSec: data.duration_sec || '',
				timeSec: data.time_sec || '',
				n8nFilename: data.filename || '',
				rawResponse: JSON.stringify(responseData)
			};
			
			var formData = new URLSearchParams();
			for (var key in saveData) {
				formData.append(key, saveData[key]);
			}
			
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
				body: formData
			})
			.then(response => response.json())
			.then(result => {
				console.log('Saved n8n result for', filename, ':', result);
			})
			.catch(error => {
				console.error('Error saving n8n result:', error);
			});
		}
		
		function bulkCheckStatus() {
			var selectedFiles = getSelectedFiles();
			if (selectedFiles.length === 0) {
				alert('Выберите файлы для проверки статуса!');
				return;
			}
			
			console.log('Bulk checking status for fileIds:', selectedFiles);
			
			// Check status for each file
			var completed = 0;
			var total = selectedFiles.length;
			var results = [];
			
			selectedFiles.forEach(function(fileId, index) {
				setTimeout(function() {
					checkFileStatusBulk(fileId, function(result) {
						results.push(result);
						completed++;
						
						if (completed === total) {
							console.log('All status checks completed:', results);
							// Show summary
							showBulkStatusSummary(results);
							// Reload page to show updated statuses
							setTimeout(function() {
								$$.ajax.loadContent('${form.requestUrl}', this);
							}, 1000);
						}
					});
				}, index * 500); // 0.5 second delay between checks
			});
		}
		
		function checkFileStatusBulk(fileId, callback) {
			// First, get saved results to find the hash
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
				body: 'method=getAixFlowResults&responseType=json'
			})
			.then(response => response.json())
			.then(data => {
				var results = data.data || data;
				if (!results.success || !results.results) {
					callback({filename: filename, error: 'Failed to get saved results'});
					return;
				}
				
				// Find result for this file by matching filename from row
				var row = document.querySelector('[data-fileid="' + fileId + '"]') || document.querySelector('tr input[name="selectedFiles"][value="'+fileId+'"]').closest('tr');
				var filename = row ? row.getAttribute('data-filename') : null;
				var fileResult = results.results.find(function(r) { return r.filename === filename; });
				
				if (!fileResult || !fileResult.hash) {
					callback({filename: filename, error: 'No n8n result found for this file'});
					return;
				}
				
				// Get n8n config
				fetch('${form.requestURI}', {
					method: 'POST',
					headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
					body: 'method=getN8nConfig&responseType=json'
				})
				.then(response => response.json())
				.then(configData => {
					var config = configData.data || configData;
					if (!config.success) {
						callback({filename: filename, error: 'Failed to get n8n config'});
						return;
					}
					
					// Check status with n8n
					var statusPayload = {
						hash: fileResult.hash,
						action: 'transkribe_getTaskData'
					};
					
					var auth = btoa(config.n8nAuthUsername + ':' + config.n8nAuthPassword);
					
					fetch(config.n8nUrl, {
						method: 'POST',
						headers: {
							'Content-Type': 'application/json',
							'Authorization': 'Basic ' + auth
						},
						body: JSON.stringify(statusPayload)
					})
					.then(response => response.json())
					.then(responseData => {
						console.log('Status check response for fileId', fileId, ':', responseData);
						
						// Update result in database
						if (responseData.data) {
							updateN8nResultBulk(filename, fileResult.hash, responseData);
						}
						
						callback({
						filename: filename,
							status: responseData.data ? responseData.data.status : 'unknown',
							success: true
						});
					})
					.catch(error => {
						console.error('Error checking status:', error);
						callback({filename: filename, error: 'Network error'});
					});
				})
				.catch(error => {
					console.error('Error getting n8n config:', error);
					callback({filename: filename, error: 'Config error'});
				});
			})
			.catch(error => {
				console.error('Error getting saved results:', error);
				callback({filename: filename, error: 'Database error'});
			});
		}
		
		function updateN8nResultBulk(filename, hash, responseData) {
			var data = responseData.data || {};
			
			var updateData = {
				method: 'saveN8nResult',
				responseType: 'json',
				filename: filename,
				hash: hash,
				status: data.status || 'processing',
				text: data.text || '',
				durationSec: data.duration_sec || '',
				timeSec: data.time_sec || '',
				n8nFilename: data.filename || '',
				rawResponse: JSON.stringify(responseData)
			};
			
			var formData = new URLSearchParams();
			for (var key in updateData) {
				formData.append(key, updateData[key]);
			}
			
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
				body: formData
			})
			.then(response => response.json())
			.then(result => {
				console.log('Updated n8n result for', filename, ':', result);
			})
			.catch(error => {
				console.error('Error updating n8n result:', error);
			});
		}
		
		function showBulkStatusSummary(results) {
			var summary = 'Результаты проверки статуса:\\n\\n';
			var completed = 0;
			var queued = 0;
			var processing = 0;
			var errors = 0;
			
			results.forEach(function(result) {
				if (result.error) {
					summary += '❌ ' + result.filename + ': ' + result.error + '\\n';
					errors++;
				} else {
					summary += '📊 ' + result.filename + ': ' + result.status + '\\n';
					if (result.status === 'completed') completed++;
					else if (result.status === 'queued') queued++;
					else if (result.status === 'processing') processing++;
					else errors++;
				}
			});
			
			summary += '\\nИтого: ✅ ' + completed + ' завершено, 📋 ' + queued + ' в очереди, 🔄 ' + processing + ' обрабатывается, ❌ ' + errors + ' ошибок';
			
			alert(summary);
		}
		
		function bulkDeleteFiles() {
			var selectedFiles = getSelectedFiles();
			if (selectedFiles.length === 0) {
				alert('Выберите файлы для удаления!');
				return;
			}
			
			console.log('Bulk deleting fileIds:', selectedFiles);
			
			// Delete files one by one
			var completed = 0;
			var total = selectedFiles.length;
			
			selectedFiles.forEach(function(fileId, index) {
				setTimeout(function() {
					deleteFileById(fileId);
					completed++;
					
					if (completed === total) {
						console.log('All files deleted');
						// Reload page after all files are deleted
						setTimeout(function() {
							$$.ajax.loadContent('${form.requestUrl}', this);
						}, 1000);
					}
				}, index * 500); // 0.5 second delay between files
			});
		}
		
		function getSelectedFiles() {
			var selectedFiles = [];
			$('#${bulkTableUiid} input[name="selectedFiles"]:checked').each(function() {
				selectedFiles.push(parseInt($(this).val(), 10));
			});
			return selectedFiles;
		}
		
		function deleteFileById(fileId) {
			var deleteUrl = '${form.requestURI}?method=deleteFileAixflow&fileId=' + encodeURIComponent(fileId) + '&responseType=json';
			
			$.ajax({
				url: deleteUrl,
				type: 'POST',
				success: function(response) {
					console.log('File deleted by id:', fileId);
				},
				error: function(xhr, status, error) {
					console.error('Error deleting file id:', fileId, error);
				}
			});
		}
		
		function checkForNewlyUploadedFile() {
			var newlyUploadedFile = localStorage.getItem('newlyUploadedFile');
			if (newlyUploadedFile) {
				console.log('Found newly uploaded file in localStorage:', newlyUploadedFile);
				
				// Wait a bit for the page to fully load
				setTimeout(function() {
					highlightNewFile(newlyUploadedFile);
					// Clear the flag
					localStorage.removeItem('newlyUploadedFile');
				}, 1000);
			}
		}
		
		function initializeDragAndDrop() {
			var dropZone = document.getElementById('dropZone');
			var fileInput = document.getElementById('fileInput');
			
			console.log('initializeDragAndDrop called');
			console.log('dropZone:', dropZone);
			console.log('fileInput:', fileInput);
			
			if (!dropZone) {
				console.error('Drop zone not found!');
				return;
			}
			
			if (!fileInput) {
				console.error('File input not found!');
				return;
			}
			
			// Click to select files
			dropZone.addEventListener('click', function(e) {
				console.log('Drop zone clicked, triggering file input');
				console.log('Event:', e);
				console.log('FileInput exists:', !!fileInput);
				console.log('FileInput click method exists:', typeof fileInput.click);
				
				try {
					fileInput.click();
					console.log('File input click triggered successfully');
				} catch (error) {
					console.error('Error triggering file input click:', error);
				}
			});
			
			// File input change
			fileInput.addEventListener('change', function(e) {
				console.log('File input changed, files:', e.target.files);
				handleFiles(e.target.files);
			});
			
			// Drag events
			dropZone.addEventListener('dragover', function(e) {
				e.preventDefault();
				e.stopPropagation();
				dropZone.classList.add('drag-over');
			});
			
			dropZone.addEventListener('dragleave', function(e) {
				e.preventDefault();
				e.stopPropagation();
				dropZone.classList.remove('drag-over');
			});
			
			dropZone.addEventListener('drop', function(e) {
				e.preventDefault();
				e.stopPropagation();
				dropZone.classList.remove('drag-over');
				
				var files = e.dataTransfer.files;
				handleFiles(files);
			});
		}
		
		function handleFiles(files) {
			if (!files || files.length === 0) return;
			
			var index = 0;
			function next() {
				if (index >= files.length) {
					// Reload page content after all files are uploaded
					setTimeout(function() { $$.ajax.loadContent('${form.requestUrl}', this); }, 500);
					return;
				}
				var file = files[index++];
				if (!isAudioFile(file)) { next(); return; }
				uploadFileBatch(file, function(success) { 
					if (success) { localStorage.setItem('newlyUploadedFile', file.name); }
					next(); 
				});
			}
			next();
		}
		
		function isAudioFile(file) {
			var audioTypes = ['audio/mpeg', 'audio/wav', 'audio/mp4', 'audio/ogg', 'audio/flac'];
			var audioExtensions = ['.mp3', '.wav', '.m4a', '.ogg', '.flac'];
			
			// Check MIME type
			if (audioTypes.includes(file.type)) {
				return true;
			}
			
			// Check file extension
			var fileName = file.name.toLowerCase();
			return audioExtensions.some(ext => fileName.endsWith(ext));
		}
		
		function uploadFile(file) {
			var dropZone = document.getElementById('dropZone');
			var dropZoneContent = document.getElementById('dropZoneContent');
			var dropZoneProgress = document.getElementById('dropZoneProgress');
			var progressBar = document.getElementById('progressBar');
			var progressText = document.getElementById('progressText');
			
			// Show progress
			dropZoneContent.style.display = 'none';
			dropZoneProgress.style.display = 'block';
			dropZone.classList.add('uploading');
			
			// Create FormData
			var formData = new FormData();
			formData.append('method', 'uploadFile');
			formData.append('responseType', 'json');
			formData.append('file', file);
			
			// Create XMLHttpRequest for progress tracking
			var xhr = new XMLHttpRequest();
			
			// Progress tracking
			xhr.upload.addEventListener('progress', function(e) {
				if (e.lengthComputable) {
					var percentComplete = (e.loaded / e.total) * 100;
					progressBar.style.width = percentComplete + '%';
					progressText.textContent = Math.round(percentComplete) + '%';
				}
			});
			
			// Upload complete
			xhr.addEventListener('load', function() {
				if (xhr.status === 200) {
					try {
						var response = JSON.parse(xhr.responseText);
						if (response.status === 'ok') {
							// Success
							progressBar.style.width = '100%';
							progressText.textContent = '100%';
							
							// For single upload via programmatic call, keep old behavior
							localStorage.setItem('newlyUploadedFile', file.name);
							setTimeout(function() { $$.ajax.loadContent('${form.requestUrl}', this); }, 500);
						} else {
							showUploadError('Ошибка загрузки: ' + (response.message || 'Неизвестная ошибка'));
						}
					} catch (e) {
						showUploadError('Ошибка обработки ответа сервера');
					}
				} else {
					showUploadError('Ошибка сервера: ' + xhr.status);
				}
			});
			
			// Upload error
			xhr.addEventListener('error', function() {
				showUploadError('Ошибка сети при загрузке файла');
			});
			
			// Start upload
			xhr.open('POST', '${form.requestURI}');
			xhr.send(formData);
		}

		// Batch version used by handleFiles: no reload; calls callback when done
		function uploadFileBatch(file, callback) {
			var dropZone = document.getElementById('dropZone');
			var dropZoneContent = document.getElementById('dropZoneContent');
			var dropZoneProgress = document.getElementById('dropZoneProgress');
			var progressBar = document.getElementById('progressBar');
			var progressText = document.getElementById('progressText');
			
			// Show progress for current file
			dropZoneContent.style.display = 'none';
			dropZoneProgress.style.display = 'block';
			dropZone.classList.add('uploading');
			
			var formData = new FormData();
			formData.append('method', 'uploadFile');
			formData.append('responseType', 'json');
			formData.append('file', file);
			
			var xhr = new XMLHttpRequest();
			xhr.upload.addEventListener('progress', function(e) {
				if (e.lengthComputable) {
					var percentComplete = (e.loaded / e.total) * 100;
					progressBar.style.width = percentComplete + '%';
					progressText.textContent = Math.round(percentComplete) + '%';
				}
			});
			xhr.addEventListener('load', function() {
				var ok = false;
				if (xhr.status === 200) {
					try {
						var response = JSON.parse(xhr.responseText);
						ok = response.status === 'ok';
					} catch (e) { ok = false; }
				}
				// Reset progress for next file
				progressBar.style.width = '0%';
				progressText.textContent = '0%';
				dropZone.classList.remove('uploading');
				dropZoneContent.style.display = 'block';
				dropZoneProgress.style.display = 'none';
				if (typeof callback === 'function') callback(ok);
			});
			xhr.addEventListener('error', function() { if (typeof callback === 'function') callback(false); });
			xhr.open('POST', '${form.requestURI}');
			xhr.send(formData);
		}
		
		function highlightNewFile(filename) {
			console.log('Highlighting new file:', filename);
			
			// Try multiple ways to find the file row
			var fileRow = null;
			
			// Method 1: Search by onclick attribute
			var fileRows = document.querySelectorAll('tr[onclick*="' + filename + '"]');
			if (fileRows.length > 0) {
				fileRow = fileRows[0];
			}
			
			// Method 2: Search by text content
			if (!fileRow) {
				var allRows = document.querySelectorAll('tr');
				for (var i = 0; i < allRows.length; i++) {
					if (allRows[i].textContent && allRows[i].textContent.includes(filename)) {
						fileRow = allRows[i];
						break;
					}
				}
			}
			
			// Method 3: Search in file details panels
			if (!fileRow) {
				var detailsPanels = document.querySelectorAll('.file-info-panel');
				for (var i = 0; i < detailsPanels.length; i++) {
					if (detailsPanels[i].textContent && detailsPanels[i].textContent.includes(filename)) {
						fileRow = detailsPanels[i].closest('tr');
						break;
					}
				}
			}
			
			if (!fileRow) {
				console.log('File row not found for:', filename);
				console.log('Available rows:', document.querySelectorAll('tr').length);
				return;
			}
			
			console.log('Found file row:', fileRow);
			
			// Add highlight class
			fileRow.classList.add('new-file-highlight');
			
			// Remove highlight after 3 seconds
			setTimeout(function() {
				fileRow.classList.remove('new-file-highlight');
			}, 3000);
		}
		
		function showUploadError(message) {
			var dropZone = document.getElementById('dropZone');
			var dropZoneContent = document.getElementById('dropZoneContent');
			var dropZoneProgress = document.getElementById('dropZoneProgress');
			
			dropZone.classList.remove('uploading');
			dropZone.classList.add('error');
			
			dropZoneProgress.style.display = 'none';
			dropZoneContent.style.display = 'block';
			dropZoneContent.innerHTML = 
				'<div style="font-size: 48px; margin-bottom: 10px;">❌</div>' +
				'<div style="font-size: 18px; font-weight: bold; margin-bottom: 5px; color: #dc3545;">Ошибка загрузки</div>' +
				'<div style="font-size: 14px; color: #666;">' + message + '</div>';
			
			// Reset after 3 seconds
			setTimeout(function() {
				dropZone.classList.remove('error');
				dropZoneContent.innerHTML = 
					'<div style="font-size: 48px; margin-bottom: 10px;">📁</div>' +
					'<div style="font-size: 18px; font-weight: bold; margin-bottom: 5px;">Перетащите файлы сюда</div>' +
					'<div style="font-size: 14px; color: #666;">или нажмите для выбора файлов</div>';
			}, 3000);
		}
		
		// Load saved results on page load
		if (document.readyState === 'loading') {
			document.addEventListener('DOMContentLoaded', function() {
				loadSavedResults();
			});
		} else {
			// DOM is already loaded
			loadSavedResults();
		}
		
		function loadSavedResults() {
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: 'method=getAixFlowResults&responseType=json'
			})
			.then(response => response.json())
			.then(response => {
				var data = response.data || response;
				
				if (data.success && data.results) {
					// Get current file list
					var currentFiles = [];
					var fileRows = document.querySelectorAll('[data-filename]');
					fileRows.forEach(function(row) {
						var filename = row.getAttribute('data-filename');
						if (filename) {
							currentFiles.push(filename);
						}
					});
					
					data.results.forEach(function(result) {
						// Only show results for files that currently exist
						if (currentFiles.includes(result.filename)) {
							savedResults[result.filename] = result;
							
							// Cache text data for analytics panel
							textDataCache[result.filename] = {
								text: result.text,
								diarizationResult: result.diarizationResult
							};
							
							// Show completed status or add check button
							if (result.status === 'completed' || result.status === 'error') {
								addCompletedStatusButton(result.filename, result);
							} else {
								addStatusCheckButton(result.filename, result.hash);
							}
						}
					});
					
					// Apply status highlighting after loading results
					applyStatusHighlighting();
				}
			})
			.catch(error => {
				console.error('Failed to load saved results:', error);
			});
		}
		
		function toggleFileDetails(filename) {
			var detailsRow = document.getElementById('details-' + filename);
			var icon = document.getElementById('icon-' + filename);
			
			if (detailsRow.style.display === 'none') {
				detailsRow.style.display = '';
				icon.className = 'ti-angle-up';
			} else {
				detailsRow.style.display = 'none';
				icon.className = 'ti-angle-down';
			}
		}

		// helper to expand/collapse long diarization text
		function toggleDiarText(btn) {
			var t = btn.previousElementSibling;
			if (!t) return false;
			var c = t.classList;
			if (c.contains('collapsed')) {
				c.remove('collapsed');
				c.add('expanded');
				btn.textContent = 'Свернуть';
			} else {
				c.remove('expanded');
				c.add('collapsed');
				btn.textContent = 'Развернуть';
			}
			return false;
		}

		function toggleAnalyticsTextMode(mode, filename) {
			var textContentDiv = document.getElementById('analytics-text-content-' + filename);
			if (!textContentDiv) return;
			
			// Get the data for this file
			var fileData = textDataCache[filename];
			if (!fileData) return;
			
			var textToShow = '';
			var hasDiarization = fileData.diarizationResult && fileData.diarizationResult.trim() !== '';
			var hasTranscription = fileData.text && fileData.text.trim() !== '' && fileData.text !== 'null';
			
			// Determine what text to show based on mode
			if (mode === 'transcription') {
				textToShow = hasTranscription ? '"' + fileData.text + '"' : '<em>Текст транскрибации недоступен</em>';
				// Style for transcription (regular text)
				textContentDiv.style.whiteSpace = 'normal';
				textContentDiv.style.fontFamily = 'inherit';
				textContentDiv.style.fontSize = '14px';
				textContentDiv.style.lineHeight = '1.5';
			} else if (mode === 'diarization') {
				textToShow = hasDiarization ? formatDiarizationText(fileData.diarizationResult) : '<em>Диаризация недоступна</em>';
				// Style for diarization (preserve formatting)
				textContentDiv.style.whiteSpace = 'pre-wrap';
				textContentDiv.style.fontFamily = 'monospace';
				textContentDiv.style.fontSize = '13px';
				textContentDiv.style.lineHeight = '1.4';
			}
			
			// Update the content
			textContentDiv.innerHTML = textToShow;
			
			// Update button states
			var transcriptionBtn = textContentDiv.parentElement.querySelector('button[onclick*="transcription"]');
			var diarizationBtn = textContentDiv.parentElement.querySelector('button[onclick*="diarization"]');
			
			if (mode === 'transcription') {
				transcriptionBtn.style.backgroundColor = '#007bff';
				transcriptionBtn.style.color = 'white';
				diarizationBtn.style.backgroundColor = '';
				diarizationBtn.style.color = '';
			} else if (mode === 'diarization') {
				diarizationBtn.style.backgroundColor = '#6f42c1';
				diarizationBtn.style.color = 'white';
				transcriptionBtn.style.backgroundColor = '';
				transcriptionBtn.style.color = '';
			}
		}

		function formatDiarizationText(text) {
			if (!text || text.trim() === '') return text;
			
			console.log('=== DIARIZATION FORMAT DEBUG ===');
			console.log('Raw text:', text);
			console.log('Text type:', typeof text);
			console.log('Text length:', text.length);
			console.log('First 200 chars:', text.substring(0, 200));
			
			try {
				// Try to parse as JSON array first
				var diarizationData = JSON.parse(text);
				console.log('Parsed as JSON:', diarizationData);
				console.log('Is array:', Array.isArray(diarizationData));
				
				if (Array.isArray(diarizationData)) {
					console.log('Using formatDiarizationAsCards');
					return formatDiarizationAsCards(diarizationData);
				}
			} catch (e) {
				console.log('Not JSON, using fallback format:', e.message);
			}
			
			console.log('Using fallback text format');
			console.log('=== END DIARIZATION FORMAT DEBUG ===');
			
			// Convert plain text to card format
			return formatPlainTextAsCards(text);
		}
		
		function formatPlainTextAsCards(text) {
			if (!text || text.trim() === '') {
				return '<div class="diarization-text">Диаризация недоступна</div>';
			}
			
			var lines = text.split('\n');
			var messages = [];
			
			lines.forEach(function(line) {
				if (line.trim() === '') return;
				
				// Check if line starts with speaker name
				var speakerMatch = line.match(/^([^:]+):\s*(.*)$/);
				if (speakerMatch) {
					var speaker = speakerMatch[1].trim();
					var content = speakerMatch[2].trim();
					
					messages.push({
						speaker: speaker,
						text: content
					});
				}
			});
			
			if (messages.length === 0) {
				return '<div class="diarization-text">Диаризация недоступна</div>';
			}
			
			return formatDiarizationAsCards(messages);
		}
		
		function formatDiarizationAsCards(diarizationData) {
			if (!Array.isArray(diarizationData) || diarizationData.length === 0) {
				return '<div class="diarization-text">Диаризация недоступна</div>';
			}
			
			console.log('=== FORMAT CARDS DEBUG ===');
			console.log('Diarization data:', diarizationData);
			console.log('Data length:', diarizationData.length);
			
			var html = '<div class="diarization-container">';
			html += '<div class="diarization-header">Речевая аналитика</div>';
			html += '<div class="diarization-content">';
			
			// Count speakers
			var speakerCounts = {};
			var totalMessages = diarizationData.length;
			
			diarizationData.forEach(function(item) {
				if (item.speaker) {
					speakerCounts[item.speaker] = (speakerCounts[item.speaker] || 0) + 1;
				}
			});
			
			console.log('Speaker counts:', speakerCounts);
			console.log('Total messages:', totalMessages);
			
			// Format each message
			diarizationData.forEach(function(item, index) {
				var speaker = item.speaker || 'Неизвестный';
				var text = item.text || '';
				
				console.log('Processing message', index, ':', speaker, ':', text);
				
				// Determine speaker class
				var speakerClass = 'speaker-abonent'; // Default
				if (speaker.toLowerCase().includes('оператор')) {
					speakerClass = 'speaker-operator';
				} else if (speaker.toLowerCase().includes('абонент')) {
					speakerClass = 'speaker-customer';
				}
				
				console.log('Speaker class:', speakerClass);
				
				html += '<div class="diarization-item ' + speakerClass + '">';
				html += '<div class="speaker-label">' + speaker + '</div>';
				// длинные реплики сворачиваем, добавляем переключатель
				var collapsed = (text && text.length > 220) ? ' collapsed' : '';
				html += '<div class="speaker-text' + collapsed + '">' + text + '</div>';
				if (collapsed) {
					html += '<button class="diarization-toggle" onclick="return toggleDiarText(this);">Развернуть</button>';
				}
				html += '</div>';
			});
			
			html += '</div>';
			
			// Add stats
			html += '<div class="diarization-stats">';
			html += '<div class="diarization-stats-item">Всего сообщений: ' + totalMessages + '</div>';
			
			var speakerStats = Object.keys(speakerCounts).map(function(speaker) {
				return speaker + ': ' + speakerCounts[speaker];
			}).join(', ');
			
			html += '<div class="diarization-stats-item">' + speakerStats + '</div>';
			html += '</div>';
			html += '</div>';
			
			console.log('Generated HTML length:', html.length);
			console.log('Generated HTML preview:', html.substring(0, 500));
			console.log('=== END FORMAT CARDS DEBUG ===');
			
			return html;
		}

		function playAudio(url, filename) {
			// Remove existing audio player if any
			var existingPlayer = document.getElementById('audio-player');
			if (existingPlayer) {
				existingPlayer.remove();
			}

			// Create audio player
			var player = document.createElement('div');
			player.id = 'audio-player';
			player.className = 'audio-player';
			player.innerHTML = 
				'<h4>🔊 ' + filename + '</h4>' +
				'<audio controls autoplay style="width: 100%;">' +
				'<source src="' + url + '" type="audio/' + getFileExtension(filename) + '">' +
				'Your browser does not support the audio element.' +
				'</audio>' +
				'<button onclick="this.parentNode.remove()" class="btn-grey mt05">Close</button>';
			
			// Style the player
			player.style.cssText = 
				'position: fixed; ' +
				'top: 50%; ' +
				'left: 50%; ' +
				'transform: translate(-50%, -50%); ' +
				'background: white; ' +
				'padding: 20px; ' +
				'border: 2px solid #007bff; ' +
				'border-radius: 8px; ' +
				'box-shadow: 0 4px 20px rgba(0,0,0,0.3); ' +
				'z-index: 1000; ' +
				'min-width: 400px; ' +
				'max-width: 80vw; ' +
				'box-sizing: border-box; ' +
				'overflow: visible; ' +
				'position: relative; ' +
				'z-index: 1000;';
			
			// Insert after the files table
			var table = document.querySelector('.data');
			table.parentNode.insertBefore(player, table.nextSibling);
		}

		function playVideo(url, filename) {
			// Remove existing video player if any
			var existingPlayer = document.getElementById('video-player');
			if (existingPlayer) {
				existingPlayer.remove();
			}

			// Create video player
			var player = document.createElement('div');
			player.id = 'video-player';
			player.className = 'video-player';
			player.innerHTML = 
				'<h4>🎬 ' + filename + '</h4>' +
				'<video controls autoplay style="max-width: 100%; max-height: 400px;">' +
				'<source src="' + url + '" type="video/' + getFileExtension(filename) + '">' +
				'Your browser does not support the video element.' +
				'</video>' +
				'<button onclick="this.parentNode.remove()" class="btn-grey mt05">Close</button>';
			
			// Insert after the files table
			var table = document.querySelector('.data');
			table.parentNode.insertBefore(player, table.nextSibling);
		}

		function viewImage(url, filename) {
			// Open image in new tab
			window.open(url, '_blank');
		}

		function getFileExtension(filename) {
			return filename.split('.').pop().toLowerCase();
		}

		// Store task hashes for status checking
		var taskHashes = {};
		
		// Test function to check if AJAX is working
		
		function cleanupN8nResults() {
			if (!confirm('Cleanup orphaned n8n results? This will delete results for files that no longer exist.')) {
				return;
			}
			
			
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: 'method=cleanupAixFlowResults&responseType=json'
			})
			.then(response => response.json())
			.then(response => {
				var data = response.data || response;
				
				if (data.success) {
					alert('Cleanup completed successfully!\n\n' + data.message);
					// Reload saved results to reflect changes
					loadSavedResults();
				} else {
					alert('Cleanup failed: ' + data.message);
				}
			})
			.catch(error => {
				console.error('Failed to cleanup n8n results:', error);
				alert('Failed to cleanup n8n results: ' + error.message);
			});
		}
		
		function updateDatabaseSchema() {
			if (!confirm('Update database schema? This will add the raw_response column if it doesn\'t exist.')) {
				return;
			}
			
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: 'method=updateDatabaseSchema&responseType=json'
			})
			.then(response => response.json())
			.then(response => {
				var data = response.data || response;
				
				if (data.success) {
					alert('Database schema updated successfully!\n\n' + data.message);
				} else {
					alert('Schema update failed: ' + data.message);
				}
			})
			.catch(error => {
				console.error('Failed to update database schema:', error);
				alert('Failed to update database schema: ' + error.message);
			});
		}
		

		function sendFileToAIXFLOW(filename) { return sendFileToN8n(filename); }
		function sendFileToTranskribe(filename) { return sendFileToAIXFLOW(filename); }

		function sendFileToN8n(filename) {
			if (!filename || filename.trim() === '') {
				alert('Filename is empty!');
				return;
			}
			
			if (!confirm('${l.l("Send file to AIXFLOW for processing?")}')) {
				return;
			}

			var button = event.target;
			var originalText = button.innerHTML;
			button.innerHTML = '⏳ ${l.l("Sending...")}';
			button.disabled = true;

			// Get AIXFLOW configuration from server first
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: 'method=getN8nConfig&responseType=json'
			})
			.then(response => response.json())
			.then(response => {
				var config = response.data || response;
				
				if (!config.n8nUrl) {
					alert('AIXFLOW URL is not configured');
					button.innerHTML = originalText;
					button.disabled = false;
					return;
				}
				
				// Read file and send to AIXFLOW
				sendFileDirectlyToN8n(filename, config, button, originalText);
			})
			.catch(error => {
				alert('Failed to get AIXFLOW configuration: ' + error.message);
				button.innerHTML = originalText;
				button.disabled = false;
			});
		}
		
		function sendFileDirectlyToN8n(filename, config, button, originalText) {
			// Get file content from kernel storage via /user/file.do
			var row = document.querySelector('[data-filename="' + filename + '"]');
			if (!row) {
				alert('File row not found');
				button.innerHTML = originalText;
				button.disabled = false;
				return;
			}
			var fileId = row.getAttribute('data-fileid');
			var title = row.getAttribute('data-title');
			var secret = row.getAttribute('data-secret');
			var downloadUrl = '/user/file.do?id=' + encodeURIComponent(fileId) + '&title=' + encodeURIComponent(title) + '&secret=' + encodeURIComponent(secret);
			
			fetch(downloadUrl)
				.then(response => response.blob())
				.then(blob => {
					// Convert blob to base64
					var reader = new FileReader();
					reader.onload = function() {
						var base64 = reader.result.split(',')[1]; // Remove data:audio/mpeg;base64, prefix
						
						// Prepare request data
						var requestData = {
							fileSize: blob.size,
							taskId: generateUUID(),
							fileContent: base64,
							action: 'transkribe'
						};
						
						console.log('=== SEND FILE TO N8N REQUEST DEBUG ===');
						console.log('Filename:', filename);
						console.log('File size:', blob.size);
						console.log('Task ID:', requestData.taskId);
						console.log('N8n URL:', config.n8nUrl);
						console.log('Request data (without base64):', {
							fileSize: requestData.fileSize,
							taskId: requestData.taskId,
							fileContent: '[BASE64_DATA_' + base64.length + '_chars]'
						});
						console.log('=== END SEND FILE TO N8N REQUEST DEBUG ===');
						
						// Send to n8n
						var headers = {
							'Content-Type': 'application/json'
						};
						
						// Add Basic Auth if configured
						if (config.n8nAuthUsername && config.n8nAuthPassword) {
							var auth = btoa(config.n8nAuthUsername + ':' + config.n8nAuthPassword);
							headers['Authorization'] = 'Basic ' + auth;
						}
						
						fetch(config.n8nUrl, {
							method: 'POST',
							headers: headers,
							body: JSON.stringify(requestData)
						})
						.then(response => response.json())
						.then(data => {
							console.log('=== SEND FILE TO N8N RESPONSE DEBUG ===');
							console.log('Full response:', data);
							
							// Check if we have data field in response
							var responseData = data.data || data;
							console.log('Processed responseData:', responseData);
							
							// Handle new format with transkribe block
							var actualData = responseData;
							if (responseData.transkribe) {
								actualData = responseData.transkribe;
								console.log('Using transkribe block:', actualData);
							}
							
							console.log('actualData.hash:', actualData.hash);
							console.log('actualData.status:', actualData.status);
							console.log('=== END SEND FILE TO N8N RESPONSE DEBUG ===');
							
							if (actualData.hash) {
								alert('${l.l("File sent successfully!")}\nHash: ' + actualData.hash + '\nStatus: ' + actualData.status);
								
								// Store hash for status checking
								taskHashes[filename] = actualData.hash;
								
								// Save result to database
								saveN8nResult(filename, actualData);
								
								// Add status button or show completed status
								if (actualData.status === 'completed' || actualData.status === 'error') {
									addCompletedStatusButton(filename, actualData);
								} else {
									addStatusCheckButton(filename, actualData.hash);
								}
							} else {
								alert('${l.l("Error:")} ' + (data.message || 'Unknown error from n8n'));
							}
						})
						.catch(error => {
							console.error('N8n request error:', error);
							alert('${l.l("Failed to send file to N8n")}\nError: ' + error.message);
						})
						.finally(() => {
							button.innerHTML = originalText;
							button.disabled = false;
						});
					};
					reader.readAsDataURL(blob);
				})
				.catch(error => {
					console.error('File read error:', error);
					alert('Failed to read file: ' + error.message);
					button.innerHTML = originalText;
					button.disabled = false;
				});
		}
		
		function generateUUID() {
			return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
				var r = Math.random() * 16 | 0;
				var v = c == 'x' ? r : (r & 0x3 | 0x8);
				return v.toString(16);
			});
		}
		
		function saveN8nResult(filename, responseData) {
			
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: 'method=saveAixFlowResult&filename=' + encodeURIComponent(filename) + 
					  '&hash=' + encodeURIComponent(responseData.hash) +
					  '&status=' + encodeURIComponent(responseData.status) +
					  '&text=' + encodeURIComponent(responseData.text || '') +
					  '&durationSec=' + encodeURIComponent(responseData.duration_sec || '') +
					  '&timeSec=' + encodeURIComponent(responseData.time_sec || '') +
					  '&n8nFilename=' + encodeURIComponent(responseData.filename || '') +
					  '&rawResponse=' + encodeURIComponent(JSON.stringify(responseData))
			})
			.then(response => response.json())
			.then(data => {
			})
			.catch(error => {
				console.error('Failed to save n8n result:', error);
			});
		}

		function addCompletedStatusButton(filename, responseData) {
			var fileRow = document.querySelector('[data-filename="' + filename + '"]');
			if (!fileRow) {
				return;
			}
			
			// Update saved results
			savedResults[filename] = responseData;

			var detailsRow = document.getElementById('details-' + filename);
			if (!detailsRow) {
				return;
			}

			var buttonContainer = detailsRow.querySelector('.file-info-panel > div:last-child');
			if (!buttonContainer) {
				return;
			}

			// Remove existing status button
			var existingButton = buttonContainer.querySelector('.n8n-status-button');
			if (existingButton) {
				existingButton.remove();
			}
			
			// Remove ALL existing analytics buttons
			var existingAnalyticsButtons = buttonContainer.querySelectorAll('.btn-purple');
			existingAnalyticsButtons.forEach(function(btn) {
				if (btn.innerHTML.includes('📊')) {
					btn.remove();
				}
			});
			
			// Remove ALL existing diarization buttons
			var existingDiarizationButtons = buttonContainer.querySelectorAll('.btn-purple');
			existingDiarizationButtons.forEach(function(btn) {
				if (btn.innerHTML.includes('👥')) {
					btn.remove();
				}
			});

			// Create completed status button
			var statusButton = document.createElement('button');
			statusButton.className = responseData.status === 'completed' ? 'btn-green n8n-status-button' : 'btn-red n8n-status-button';
			statusButton.disabled = true;
			
			var statusIcon = responseData.status === 'completed' ? '✅' : '❌';
			var statusText = responseData.status === 'completed' ? 'Completed' : 'Error';
			statusButton.innerHTML = statusIcon + ' ' + statusText;
			
			// Add click handler to show details
			statusButton.onclick = function() {
				var message = statusIcon + ' ' + statusText + '\n\n';
				
				if (responseData.text && responseData.text !== 'null') {
					message += 'Text: ' + responseData.text + '\n\n';
				}
				if (responseData.duration_sec && responseData.duration_sec !== 'null') {
					message += 'Duration: ' + responseData.duration_sec + ' sec\n';
				}
				if (responseData.time_sec && responseData.time_sec !== 'null') {
					message += 'Time: ' + responseData.time_sec + ' sec\n';
				}
				if (responseData.filename) {
					message += 'Filename: ' + responseData.filename + '\n';
				}
				
				alert(message);
			};
			
			// Add status button first
			buttonContainer.appendChild(statusButton);
			
			// Apply status highlighting
			applyStatusHighlighting();
			
			// Add speech analytics button for completed status
			if (responseData.status === 'completed') {
				var analyticsButton = document.createElement('button');
				analyticsButton.className = 'btn-purple mt05';
				analyticsButton.innerHTML = '📊 Речевая аналитика';
				analyticsButton.type = 'button';
				analyticsButton.onclick = function(event) {
					event.preventDefault();
					event.stopPropagation();
					showSpeechAnalytics(filename, responseData);
					return false;
				};
				buttonContainer.appendChild(analyticsButton);
			}
		}

		function showSpeechAnalytics(filename, responseData) {
			
			// Remove existing analytics panel
			var existingPanel = document.getElementById('analytics-' + filename);
			if (existingPanel) {
				existingPanel.remove();
				return;
			}
			
			// Parse raw response if available
			var additionalFields = {};
			if (responseData.rawResponse) {
				try {
					var rawData = JSON.parse(responseData.rawResponse);
					additionalFields = rawData;
				} catch (e) {
					console.error('Failed to parse raw response:', e);
				}
			}
			
			// Create analytics panel
			var analyticsPanel = document.createElement('div');
			analyticsPanel.id = 'analytics-' + filename;
			analyticsPanel.className = 'speech-analytics-panel';
			analyticsPanel.style.cssText = 
				'margin-top: 10px; ' +
				'padding: 15px; ' +
				'background: #f8f9fa; ' +
				'border: 1px solid #dee2e6; ' +
				'border-radius: 8px; ' +
				'font-family: Arial, sans-serif; ' +
				'width: 100%; ' +
				'box-sizing: border-box; ' +
				'overflow: visible; ' +
				'position: relative; ' +
				'z-index: 1000;';
			
			// Analyze speech data
			var text = responseData.text || '';
			if (text === 'null' || text === null) {
				text = '';
			}
			
			// Determine default text to show (prefer diarization if available)
			var hasDiarization = responseData.diarizationResult && responseData.diarizationResult.trim() !== '';
			var hasTranscription = text && text.trim() !== '';
			var defaultText = '';
			var defaultMode = '';
			
			if (hasDiarization) {
				defaultText = formatDiarizationText(responseData.diarizationResult);
				defaultMode = 'diarization';
			} else if (hasTranscription) {
				defaultText = '"' + text + '"';
				defaultMode = 'transcription';
			} else {
				defaultText = '<em>Текст не получен от n8n</em>';
				defaultMode = 'transcription';
			}
			var duration = parseFloat(responseData.duration_sec) || 0;
			var processingTime = parseFloat(responseData.time_sec) || 0;
			
			
			// Calculate analytics
			var wordCount = text.split(/\s+/).filter(word => word.length > 0).length;
			var charCount = text.length;
			var avgWordsPerMinute = duration > 0 ? Math.round((wordCount / duration) * 60) : 0;
			var avgCharsPerMinute = duration > 0 ? Math.round((charCount / duration) * 60) : 0;
			var avgWordsPerSecond = duration > 0 ? (wordCount / duration).toFixed(2) : 0;
			var processingSpeed = processingTime > 0 ? (duration / processingTime).toFixed(2) : 0;
			
			// Detect language (simple heuristic)
			var language = detectLanguage(text);
			
			// Analyze sentiment (simple heuristic)
			var sentiment = analyzeSentiment(text);
			
			// Analyze speech patterns
			var speechPatterns = analyzeSpeechPatterns(text);
			
			// Build analytics content
			var diarizationButtonHtml = '';
			if (text && text.trim() !== '' && text !== 'null') {
				var dStatus = (responseData.diarizationStatus || '').toString();
				var hasDiar = responseData.diarizationResult && String(responseData.diarizationResult).trim() !== '';
				if (dStatus === 'processing') {
					diarizationButtonHtml = '<button type="button" class="btn-orange" data-diarize-filename="' + filename + '" disabled>⏳ Processing</button>';
				} else if (dStatus === 'completed' || hasDiar) {
					diarizationButtonHtml = '<button type="button" class="btn-green" data-diarize-filename="' + filename + '" disabled>✅ Completed</button>';
				} else {
					// no embedding of text into onclick to avoid quoting errors
					diarizationButtonHtml = '<button type="button" class="btn-purple" data-diarize-filename="' + filename + '" onclick="sendTextForDiarizationUsingSaved(\'' + filename + '\'); return false;">👥 Расставить спикеров</button>';
				}
			}
			
			var analyticsContent = 
				'<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">' +
					'<div style="display: flex; align-items: center; gap: 10px;">' +
						'<h3 style="margin: 0; color: #495057;">📊 Речевая аналитика</h3>' +
						diarizationButtonHtml +
					'</div>' +
					'<button type="button" onclick="document.getElementById(\'analytics-' + filename + '\').remove(); return false;" ' +
							'style="background: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">' +
						'✕ Закрыть' +
					'</button>' +
				'</div>' +
				
				'<div class="grid-container" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; width: 100%;">' +
					'<div style="background: white; padding: 12px; border-radius: 6px; border-left: 4px solid #007bff;">' +
						'<div style="display: flex; align-items: center; margin-bottom: 8px;">' +
							'<h4 style="margin: 0; color: #007bff;">📝 Текст</h4>' +
							'<div style="margin-left: 10px;">' +
								'<button type="button" class="btn-white btn-small" onclick="toggleAnalyticsTextMode(\'transcription\', \'' + filename + '\')" title="Показать текст транскрибации" style="margin-right: 3px;">' +
									'📄' +
								'</button>' +
								'<button type="button" class="btn-white btn-small" onclick="toggleAnalyticsTextMode(\'diarization\', \'' + filename + '\')" title="Показать диаризацию">' +
									'👥' +
								'</button>' +
							'</div>' +
						'</div>' +
						'<div id="analytics-text-content-' + filename + '" style="margin: 0; font-style: italic; color: #6c757d; white-space: pre-wrap; font-family: monospace; font-size: 13px; line-height: 1.4; max-height: 300px; overflow-y: auto; padding: 8px; background: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef;">' + defaultText + '</div>' +
					'</div>' +
					
					'<div style="background: white; padding: 12px; border-radius: 6px; border-left: 4px solid #28a745;">' +
						'<h4 style="margin: 0 0 8px 0; color: #28a745;">📊 Статистика</h4>' +
						'<ul style="margin: 0; padding-left: 20px;">' +
							'<li>Слов: <strong>' + wordCount + '</strong></li>' +
							'<li>Символов: <strong>' + charCount + '</strong></li>' +
							'<li>Язык: <strong>' + language + '</strong></li>' +
							'<li>Тональность: <strong>' + sentiment + '</strong></li>' +
						'</ul>' +
					'</div>' +
					
					'<div style="background: white; padding: 12px; border-radius: 6px; border-left: 4px solid #ffc107;">' +
						'<h4 style="margin: 0 0 8px 0; color: #ffc107;">⏱️ Время</h4>' +
						'<ul style="margin: 0; padding-left: 20px;">' +
							'<li>Длительность: <strong>' + duration + ' сек</strong></li>' +
							'<li>Время обработки: <strong>' + processingTime + ' сек</strong></li>' +
							'<li>Скорость обработки: <strong>' + processingSpeed + 'x</strong></li>' +
						'</ul>' +
					'</div>' +
					
					'<div style="background: white; padding: 12px; border-radius: 6px; border-left: 4px solid #17a2b8;">' +
						'<h4 style="margin: 0 0 8px 0; color: #17a2b8;">🗣️ Речь</h4>' +
						'<ul style="margin: 0; padding-left: 20px;">' +
							'<li>Слов/мин: <strong>' + avgWordsPerMinute + '</strong></li>' +
							'<li>Символов/мин: <strong>' + avgCharsPerMinute + '</strong></li>' +
							'<li>Слов/сек: <strong>' + avgWordsPerSecond + '</strong></li>' +
						'</ul>' +
					'</div>' +
				'</div>' +
				
				'<div style="margin-top: 15px; background: white; padding: 12px; border-radius: 6px; border-left: 4px solid #6f42c1;">' +
					'<h4 style="margin: 0 0 8px 0; color: #6f42c1;">🔍 Паттерны речи</h4>' +
					'<div style="display: flex; flex-wrap: wrap; gap: 8px;">' +
						speechPatterns.map(function(pattern) { 
							return '<span style="background: #e9ecef; padding: 4px 8px; border-radius: 12px; font-size: 12px;">' + pattern + '</span>'; 
						}).join('') +
					'</div>' +
						'</div>';
						
						// Additional fields from n8n: service info (collapsible) and DATA pretty JSON
				(function() {
					var html = '';
					var raw = null;
					if (responseData.rawResponse) { try { raw = JSON.parse(responseData.rawResponse); } catch (e) { raw = null; } }
					function row(label, valueHtml) {
						return '<div style="display:flex;gap:8px;align-items:center;margin:4px 0;">' +
							'<span style="background:#f1f3f5;border-radius:4px;padding:2px 6px;color:#495057;">📋 ' + label.toUpperCase() + '</span>' +
							'<span style="color:#343a40;">' + valueHtml + '</span>' +
						'</div>';
					}
					
					// Add diarization result if available
					if (responseData.diarizationResult && responseData.diarizationResult.trim() !== '') {
						html += '<div style="margin-top:15px;background:white;padding:12px;border-radius:6px;border-left:4px solid #6f42c1;">' +
							'<h4 style="margin:0 0 8px 0;color:#6f42c1;">👥 Результат диаризации</h4>' +
							'<div style="background:#f8f9fa;padding:10px;border-radius:4px;white-space:pre-wrap;font-family:monospace;">' + 
							responseData.diarizationResult + '</div>' +
						'</div>';
					}
					
					var service = [];
					if (raw && raw.id) service.push(row('id', String(raw.id)));
					if (raw && raw.mode) service.push(row('mode', String(raw.mode)));
					if (raw && raw.resumeUrl) service.push(row('resumeUrl', '<a href="'+raw.resumeUrl+'" target="_blank">'+raw.resumeUrl+'</a>'));
					if (raw && raw.resumeFormUrl) service.push(row('resumeFormUrl', '<a href="'+raw.resumeFormUrl+'" target="_blank">'+raw.resumeFormUrl+'</a>'));
					var dataPretty = '';
					if (raw && raw.data) {
						dataPretty = '<pre style="margin:0;white-space:pre-wrap;word-break:break-word;">'+JSON.stringify(raw.data, null, 2)+'</pre>';
					}
					if (service.length || dataPretty) {
						html += '<div style="margin-top:15px;background:white;padding:12px;border-radius:6px;border-left:4px solid #adb5bd;">' +
							'<h4 style="margin:0 0 8px 0;color:#6c757d;">⚙️ Служебные данные</h4>' +
							'<div style="margin-bottom:8px;"><button type="button" class="btn-grey" onclick="var el=document.getElementById(\'svc-'+filename+'\'); el.style.display=(el.style.display===\'none\'?\'block\':\'none\'); return false;">Показать/скрыть</button></div>' +
							'<div id="svc-'+filename+'" style="display:none;">' + service.join('') + (dataPretty ? row('data', dataPretty) : '') + '</div>' +
						'</div>';
					}
					analyticsContent += html;
				})();
			
			analyticsPanel.innerHTML = analyticsContent;
			
			// Insert inside the file details row
			var detailsRow = document.getElementById('details-' + filename);
			if (detailsRow) {
				var td = detailsRow.querySelector('td');
				if (td) {
					td.appendChild(analyticsPanel);
					
					// Initialize button states and text styles based on default mode
					setTimeout(function() {
						var transcriptionBtn = analyticsPanel.querySelector('button[onclick*="transcription"]');
						var diarizationBtn = analyticsPanel.querySelector('button[onclick*="diarization"]');
						var textContentDiv = analyticsPanel.querySelector('[id^="analytics-text-content-"]');
						
						if (defaultMode === 'diarization') {
							diarizationBtn.style.backgroundColor = '#6f42c1';
							diarizationBtn.style.color = 'white';
							// Set diarization styles
							if (textContentDiv) {
								textContentDiv.style.whiteSpace = 'pre-wrap';
								textContentDiv.style.fontFamily = 'monospace';
								textContentDiv.style.fontSize = '13px';
								textContentDiv.style.lineHeight = '1.4';
							}
						} else {
							transcriptionBtn.style.backgroundColor = '#007bff';
							transcriptionBtn.style.color = 'white';
							// Set transcription styles
							if (textContentDiv) {
								textContentDiv.style.whiteSpace = 'normal';
								textContentDiv.style.fontFamily = 'inherit';
								textContentDiv.style.fontSize = '14px';
								textContentDiv.style.lineHeight = '1.5';
							}
						}
					}, 100);
				}
			}
		}
		
		function detectLanguage(text) {
			// Simple language detection based on character patterns
			var cyrillicCount = (text.match(/[а-яё]/gi) || []).length;
			var latinCount = (text.match(/[a-z]/gi) || []).length;
			
			if (cyrillicCount > latinCount) {
				return 'Русский';
			} else if (latinCount > cyrillicCount) {
				return 'English';
			} else {
				return 'Неопределен';
			}
		}
		
		function analyzeSentiment(text) {
			// Simple sentiment analysis based on keywords
			var positiveWords = ['хорошо', 'отлично', 'прекрасно', 'замечательно', 'спасибо', 'благодарю'];
			var negativeWords = ['плохо', 'ужасно', 'проблема', 'ошибка', 'неправильно', 'не работает'];
			
			var positiveCount = positiveWords.reduce((count, word) => {
				return count + (text.toLowerCase().includes(word) ? 1 : 0);
			}, 0);
			
			var negativeCount = negativeWords.reduce((count, word) => {
				return count + (text.toLowerCase().includes(word) ? 1 : 0);
			}, 0);
			
			if (positiveCount > negativeCount) {
				return '😊 Позитивная';
			} else if (negativeCount > positiveCount) {
				return '😞 Негативная';
			} else {
				return '😐 Нейтральная';
			}
		}
		
		function analyzeSpeechPatterns(text) {
			var patterns = [];
			
			// Check for questions
			if (text.includes('?')) {
				patterns.push('❓ Вопросы');
			}
			
			// Check for exclamations
			if (text.includes('!')) {
				patterns.push('❗ Восклицания');
			}
			
			// Check for pauses (multiple spaces or dots)
			if (text.includes('...') || text.includes('  ')) {
				patterns.push('⏸️ Паузы');
			}
			
			// Check for repetition
			var words = text.toLowerCase().split(/\s+/);
			var wordCounts = {};
			words.forEach(word => {
				if (word.length > 3) {
					wordCounts[word] = (wordCounts[word] || 0) + 1;
				}
			});
			
			var repeatedWords = Object.keys(wordCounts).filter(word => wordCounts[word] > 1);
			if (repeatedWords.length > 0) {
				patterns.push('🔄 Повторы');
			}
			
			// Check for formal/informal speech
			var formalWords = ['пожалуйста', 'благодарю', 'извините', 'разрешите'];
			var hasFormalWords = formalWords.some(word => text.toLowerCase().includes(word));
			if (hasFormalWords) {
				patterns.push('🎩 Формальная речь');
			} else {
				patterns.push('👤 Неформальная речь');
			}
			
			// Check for technical terms
			var techWords = ['система', 'программа', 'компьютер', 'интернет', 'технология'];
			var hasTechWords = techWords.some(word => text.toLowerCase().includes(word));
			if (hasTechWords) {
				patterns.push('💻 Техническая лексика');
			}
			
			return patterns.length > 0 ? patterns : ['📝 Обычная речь'];
		}

		function addStatusCheckButton(filename, hash) {
			var fileRow = document.querySelector('[data-filename="' + filename + '"]');
			if (!fileRow) return;

			var detailsRow = document.getElementById('details-' + filename);
			if (!detailsRow) return;

			var buttonContainer = detailsRow.querySelector('.file-info-panel > div:last-child');
			if (!buttonContainer) return;

			// Check if status button already exists
			var existingButton = buttonContainer.querySelector('.n8n-status-button');
			if (existingButton) {
				existingButton.remove();
			}
			
			// Remove ALL existing analytics buttons
			var existingAnalyticsButtons = buttonContainer.querySelectorAll('.btn-purple');
			existingAnalyticsButtons.forEach(function(btn) {
				btn.remove();
			});

			var statusButton = document.createElement('button');
			statusButton.className = 'btn-orange n8n-status-button';
			statusButton.style.marginLeft = '5px';
			statusButton.innerHTML = '📊 ${l.l("Check Status")}';
			statusButton.onclick = function() { checkN8nTaskStatus(filename, hash); };

			buttonContainer.appendChild(statusButton);
		}

		function checkN8nTaskStatus(filename, hash) {
			var button = event.target;
			var originalText = button.innerHTML;
			button.innerHTML = '⏳ ${l.l("Checking...")}';
			button.disabled = true;

			// Get n8n configuration first
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: 'method=getN8nConfig&responseType=json'
			})
			.then(response => response.json())
			.then(response => {
				var config = response.data || response;
				
				if (!config.n8nUrl) {
					alert('N8n URL is not configured');
					button.innerHTML = originalText;
					button.disabled = false;
					return;
				}
				
				// Send status check request directly to n8n
				var requestData = {
					hash: hash,
					action: "transkribe_getTaskData"
				};
				
				var headers = {
					'Content-Type': 'application/json'
				};
				
				// Add Basic Auth if configured
				if (config.n8nAuthUsername && config.n8nAuthPassword) {
					var auth = btoa(config.n8nAuthUsername + ':' + config.n8nAuthPassword);
					headers['Authorization'] = 'Basic ' + auth;
				}
				
				fetch(config.n8nUrl, {
					method: 'POST',
					headers: headers,
					body: JSON.stringify(requestData)
				})
				.then(response => response.json())
				.then(data => {
					console.log('=== CHECK TASK STATUS RESPONSE DEBUG ===');
					console.log('Full response:', data);
					
					// Check if we have data field in response
					var taskData = data.data || data;
					console.log('Processed taskData:', taskData);
					
					// Handle new format with transkribe block
					var actualTaskData = taskData;
					if (taskData.transkribe) {
						actualTaskData = taskData.transkribe;
						console.log('Using transkribe block for task data:', actualTaskData);
					}
					
					console.log('actualTaskData.status:', actualTaskData.status);
					console.log('=== END CHECK TASK STATUS RESPONSE DEBUG ===');
					
					if (actualTaskData && actualTaskData.status) {
						var statusText = '';
						var statusIcon = '';
						
						switch(actualTaskData.status) {
						case 'processing':
							statusIcon = '⏳';
							statusText = '${l.l("Processing")}';
							break;
						case 'completed':
							statusIcon = '✅';
							statusText = '${l.l("Completed")}';
							break;
						case 'error':
							statusIcon = '❌';
							statusText = '${l.l("Error")}';
							break;
						default:
							statusIcon = '❓';
							statusText = data.status;
					}

						var message = statusIcon + ' ' + statusText + '\n\n';
						
						if (actualTaskData.text && actualTaskData.text !== 'null') {
							message += '${l.l("Text:")} ' + actualTaskData.text + '\n\n';
						}
						if (actualTaskData.duration_sec && actualTaskData.duration_sec !== 'null') {
							message += '${l.l("Duration:")} ' + actualTaskData.duration_sec + ' ${l.l("sec")}\n';
						}
						if (actualTaskData.time_sec && actualTaskData.time_sec !== 'null') {
							message += '${l.l("Time:")} ' + actualTaskData.time_sec + ' ${l.l("sec")}\n';
						}
						if (actualTaskData.filename) {
							message += '${l.l("Filename:")} ' + actualTaskData.filename + '\n';
						}

						alert(message);

						// Save updated result to database
						saveN8nResult(filename, actualTaskData);

						// Replace button with appropriate status button
						if (actualTaskData.status === 'completed' || actualTaskData.status === 'error') {
							addCompletedStatusButton(filename, actualTaskData);
						} else if (actualTaskData.status === 'processing') {
							// Update button text to show processing status
							button.innerHTML = '⏳ ${l.l("Processing")}';
							button.disabled = false;
							button.className = 'btn-orange n8n-status-button';
							button.style.marginLeft = '5px';
						}
					} else {
						alert('${l.l("Error:")} ' + (data.message || 'Unknown error from n8n'));
					}
				})
				.catch(error => {
					console.error('Status check error:', error);
					alert('${l.l("Failed to check task status")}\nError: ' + error.message);
				})
				.finally(() => {
					// Only reset button if it wasn't updated with new status
					if (button.innerHTML === '⏳ ${l.l("Checking...")}') {
						button.innerHTML = originalText;
						button.disabled = false;
					}
				});
			})
			.catch(error => {
				console.error('Failed to get n8n config:', error);
				alert('Failed to get n8n configuration: ' + error.message);
				button.innerHTML = originalText;
				button.disabled = false;
			});
		}

		function sendTextForDiarization(filename, text, hash) {
			if (!text || text.trim() === '' || text === 'null') {
				alert('${l.l("No text available for diarization")}');
				return;
			}

			if (!confirm('${l.l("Send text for speaker diarization?")}')) {
				return;
			}

			console.log('=== DIARIZATION CLIENT DEBUG ===');
			console.log('Filename:', filename);
			console.log('Text length:', text.length);
			console.log('Text content (first 100 chars):', text.substring(0, 100));
			console.log('Getting n8n config from BGERP...');
			
			// Get n8n config first
			fetch('${form.requestURI}', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: 'method=getN8nConfig&responseType=json'
			})
			.then(response => response.json())
			.then(response => {
				var config = response.data || response;
				if (!config.n8nUrl) {
					console.error('N8n URL is not configured');
					alert('${l.l("N8n URL is not configured")}');
					return;
				}

				console.log('N8n URL:', config.n8nUrl);
				console.log('Sending text directly to n8n for diarization...');
				console.log('=== END DIARIZATION CLIENT DEBUG ===');

				// Prepare request data for n8n
				var requestData = {
					text: text,
					action: 'diariz'
				};

				// Send to n8n directly
				var headers = {
					'Content-Type': 'application/json'
				};
				
				// Add Basic Auth if configured
				if (config.n8nAuthUsername && config.n8nAuthPassword) {
					var auth = btoa(config.n8nAuthUsername + ':' + config.n8nAuthPassword);
					headers['Authorization'] = 'Basic ' + auth;
				}
				
				fetch(config.n8nUrl, {
					method: 'POST',
					headers: headers,
					body: JSON.stringify(requestData)
				})
				.then(response => response.json())
				.then(data => {
					console.log('=== DIARIZATION N8N RESPONSE DEBUG ===');
					console.log('Full response:', data);
					
					// Check if we have diarization data with hash_full
					if (data.data && data.data.diariz && data.data.diariz.hash_full) {
						var diarizationHash = data.data.diariz.hash_full;
						taskHashes[filename + '_diarization'] = diarizationHash; // Store diarization hash separately
						console.log('Diarization hash_full received:', diarizationHash);
						console.log('Stored in taskHashes:', filename + '_diarization', '=', diarizationHash);
						
						// Start polling diarization status
						pollDiarizationStatus(filename, diarizationHash, config);
					} else {
						console.error('No hash_full received from n8n for diarization');
						console.log('Response structure:', JSON.stringify(data, null, 2));
						alert('${l.l("Failed to get diarization task hash")}');
					}
					console.log('=== END DIARIZATION N8N RESPONSE DEBUG ===');
				})
				.catch(error => {
					console.error('Error sending text to n8n for diarization:', error);
					alert('${l.l("Error sending text for diarization")}: ' + error.message);
				});
			})
			.catch(error => {
				console.error('Failed to get n8n config for diarization:', error);
				alert('${l.l("Failed to get n8n configuration")}');
			});
		}

		// Safe wrapper that pulls text/hash from caches to avoid inline quoting issues
		function sendTextForDiarizationUsingSaved(filename) {
			var data = (textDataCache && textDataCache[filename]) ? textDataCache[filename] : null;
			var saved = (savedResults && savedResults[filename]) ? savedResults[filename] : {};
			var text = data && data.text ? data.text : (saved.text || '');
			var hash = saved.hash || '';
			sendTextForDiarization(filename, text, hash);
		}
		
		// Function to poll diarization status
		function pollDiarizationStatus(filename, diarizationHash, config) {
			console.log('Polling diarization status for:', filename, 'diarization hash:', diarizationHash);
			
			var requestData = {
				hash: diarizationHash,
				action: "diariz_getTaskData"
			};
			
			var headers = {
				'Content-Type': 'application/json'
			};
			
			// Add Basic Auth if configured
			if (config.n8nAuthUsername && config.n8nAuthPassword) {
				var auth = btoa(config.n8nAuthUsername + ':' + config.n8nAuthPassword);
				headers['Authorization'] = 'Basic ' + auth;
			}
			
			fetch(config.n8nUrl, {
				method: 'POST',
				headers: headers,
				body: JSON.stringify(requestData)
			})
			.then(response => response.json())
			.then(data => {
				console.log('=== DIARIZATION STATUS CHECK RESPONSE DEBUG ===');
				console.log('Full response:', data);
				
				// Check if we have data field in response
				var taskData = data.data || data;
				console.log('Processed taskData:', taskData);
				
				// Handle new format with diariz block
				var actualTaskData = taskData;
				if (taskData.diariz) {
					actualTaskData = taskData.diariz;
					console.log('Using diariz block for task data:', actualTaskData);
				}
				
				console.log('actualTaskData.status:', actualTaskData.status);
				console.log('=== END DIARIZATION STATUS CHECK RESPONSE DEBUG ===');
				
				if (actualTaskData && actualTaskData.status) {
					var statusText = '';
					var statusIcon = '';
					
					switch(actualTaskData.status) {
					case 'processing':
						statusIcon = '⏳';
						statusText = '${l.l("Processing diarization")}';
						// Обновляем кнопку в шапке панели, если есть
						try {
							var headerBtn = document.querySelector('[data-diarize-filename="' + filename + '"]');
							if (headerBtn) {
								headerBtn.className = 'btn-orange';
								headerBtn.textContent = '⏳ ${l.l("Processing")}';
								headerBtn.disabled = true;
							}
						} catch(e) {}
						// Continue polling
						setTimeout(function() {
							pollDiarizationStatus(filename, diarizationHash, config);
						}, 5000); // Poll every 5 seconds
						break;
					case 'queued':
						statusIcon = '⏳';
						statusText = '${l.l("Processing diarization")}';
						try {
							var headerBtnQ = document.querySelector('[data-diarize-filename="' + filename + '"]');
							if (headerBtnQ) {
								headerBtnQ.className = 'btn-orange';
								headerBtnQ.textContent = '⏳ ${l.l("Processing")}';
								headerBtnQ.disabled = true;
							}
						} catch(e) {}
						setTimeout(function() {
							pollDiarizationStatus(filename, diarizationHash, config);
						}, 5000);
						break;
					case 'completed':
						statusIcon = '✅';
						statusText = '${l.l("Diarization completed")}';
						console.log('Diarization status:', statusIcon, statusText);
						// Обновляем кнопку в шапке панели
						try {
							var headerBtn = document.querySelector('[data-diarize-filename="' + filename + '"]');
							if (headerBtn) {
								headerBtn.className = 'btn-green';
								headerBtn.textContent = '✅ ${l.l("Completed")}';
								headerBtn.disabled = true;
							}
						} catch(e) {}
						
						// Save diarization result to database
						console.log('Saving diarization result to database...');
						fetch('${form.requestURI}', {
							method: 'POST',
							headers: {
								'Content-Type': 'application/x-www-form-urlencoded',
							},
							body: 'method=saveDiarizationResult&filename=' + encodeURIComponent(filename) +
								  '&diarizationHash=' + encodeURIComponent(diarizationHash) +
								  '&diarizationStatus=' + encodeURIComponent(actualTaskData.status) +
								  '&diarizationResult=' + encodeURIComponent(actualTaskData.result || '') +
								  '&diarizationRawResponse=' + encodeURIComponent(JSON.stringify(actualTaskData)) +
								  '&responseType=json'
						})
						.then(response => response.json())
						.then(response => {
							console.log('Diarization result saved:', response);
							var data = response.data || response;
							console.log('Processed data:', data);
							
							if (data.success) {
								console.log('Diarization result saved successfully');
								// Show success message
								alert('${l.l("Diarization completed successfully")}!\n\n' +
									  '${l.l("Result")}:\n' + (actualTaskData.result || 'No result data'));
							} else {
								console.error('Failed to save diarization result:', data.message);
								alert('${l.l("Diarization completed but failed to save result")}!\n\n' +
									  '${l.l("Error")}: ' + (data.message || 'Unknown error'));
							}
							// Stop polling and reload results
							loadSavedResults();
						})
						.catch(error => {
							console.error('Error saving diarization result:', error);
							alert('${l.l("Diarization completed but failed to save result")}!\n\n' +
								  '${l.l("Error")}: ' + error.message);
							// Stop polling and reload results
							loadSavedResults();
						});
						break;
					case 'error':
						statusIcon = '❌';
						statusText = '${l.l("Diarization error")}';
						console.log('Diarization status:', statusIcon, statusText);
						
						// Show error message
						alert('${l.l("Diarization failed")}!\n\n' +
							  '${l.l("Error")}: ' + (actualTaskData.error || 'Unknown error'));
						
						// Stop polling
						break;
					default:
						statusIcon = '❓';
						statusText = '${l.l("Unknown status")}: ' + actualTaskData.status;
						// Continue polling for unknown status
						setTimeout(function() {
							pollDiarizationStatus(filename, diarizationHash, config);
						}, 5000);
						break;
					}
					
					console.log('Diarization status:', statusIcon, statusText);
					
					// Update UI if needed (you can add UI updates here)
					// For now, just log the status
				} else {
					console.log('No status data received, continuing to poll...');
					console.log('TaskData structure:', JSON.stringify(taskData, null, 2));
					// Continue polling if no status data
					setTimeout(function() {
						pollDiarizationStatus(filename, diarizationHash, config);
					}, 5000);
				}
			})
			.catch(error => {
				console.error('Error checking diarization status:', error);
				// Continue polling on error
				setTimeout(function() {
					pollDiarizationStatus(filename, diarizationHash, config);
				}, 10000); // Poll every 10 seconds on error
			});
		}
	</script>
</div>