1949 lines
76 KiB
HTML
1949 lines
76 KiB
HTML
<!doctype html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Carica Audio</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family:
|
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.upload-container {
|
|
background: white;
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
max-width: 500px;
|
|
width: 100%;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
margin: 50px auto;
|
|
}
|
|
|
|
.result-container {
|
|
background: white;
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
max-width: 100%;
|
|
width: 100%;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
margin: 20px auto;
|
|
}
|
|
|
|
h1 {
|
|
color: #333;
|
|
margin-bottom: 10px;
|
|
text-align: center;
|
|
font-size: 24px;
|
|
}
|
|
|
|
.subtitle {
|
|
text-align: center;
|
|
color: #666;
|
|
font-size: 13px;
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
color: #555;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="password"],
|
|
textarea {
|
|
width: 100%;
|
|
padding: 14px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 10px;
|
|
font-size: 16px;
|
|
transition: all 0.3s;
|
|
font-family: inherit;
|
|
}
|
|
|
|
textarea {
|
|
resize: vertical;
|
|
min-height: 120px;
|
|
}
|
|
|
|
input[type="text"]:focus,
|
|
input[type="password"]:focus,
|
|
textarea:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
|
|
.file-input-wrapper {
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: inline-block;
|
|
width: 100%;
|
|
}
|
|
|
|
.file-input-wrapper input[type="file"] {
|
|
position: absolute;
|
|
left: -9999px;
|
|
}
|
|
|
|
.file-input-label {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border: 2px dashed #cbd5e0;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
min-height: 120px;
|
|
text-align: center;
|
|
}
|
|
|
|
.file-input-label:hover {
|
|
background: #e9ecef;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.file-input-label.has-file {
|
|
background: #e8f5e9;
|
|
border-color: #4caf50;
|
|
border-style: solid;
|
|
}
|
|
|
|
.file-input-label.converting {
|
|
background: #fff3e0;
|
|
border-color: #ff9800;
|
|
border-style: solid;
|
|
}
|
|
|
|
.file-info {
|
|
color: #666;
|
|
}
|
|
|
|
.file-info.has-file {
|
|
color: #2e7d32;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.file-info.converting {
|
|
color: #e65100;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.upload-icon {
|
|
font-size: 40px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
button {
|
|
width: 100%;
|
|
padding: 16px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
button:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: linear-gradient(135deg, #43a047 0%, #1b5e20 100%);
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
box-shadow: 0 10px 20px rgba(67, 160, 71, 0.3);
|
|
}
|
|
|
|
.btn-tertiary {
|
|
background: linear-gradient(135deg, #757575 0%, #424242 100%);
|
|
}
|
|
|
|
.btn-tertiary:hover:not(:disabled) {
|
|
box-shadow: 0 10px 20px rgba(117, 117, 117, 0.3);
|
|
}
|
|
|
|
.message {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
font-weight: 600;
|
|
display: none;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.message.success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.message.error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.message.warning {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
border: 1px solid #ffeeba;
|
|
}
|
|
|
|
.message.show {
|
|
display: block;
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.loading {
|
|
display: none;
|
|
text-align: center;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.loading.show {
|
|
display: block;
|
|
}
|
|
|
|
.spinner {
|
|
border: 3px solid #f3f3f3;
|
|
border-top: 3px solid #667eea;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.info-box {
|
|
background: #e3f2fd;
|
|
border-left: 4px solid #2196f3;
|
|
padding: 12px;
|
|
border-radius: 5px;
|
|
margin-bottom: 20px;
|
|
font-size: 13px;
|
|
color: #1565c0;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.action-buttons button {
|
|
flex: 1;
|
|
min-width: 140px;
|
|
}
|
|
|
|
#resultContent {
|
|
margin-top: 20px;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 10px;
|
|
min-height: 200px;
|
|
}
|
|
|
|
/* Stili per Editor Visuale Avanzato */
|
|
#visual-editor-container {
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
background: white;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
#visual-editor-toolbar {
|
|
display: flex !important;
|
|
flex-direction: row !important;
|
|
flex-wrap: wrap !important;
|
|
gap: 0.5rem !important;
|
|
padding: 0.75rem !important;
|
|
background: #f8f9fa !important;
|
|
border-bottom: 1px solid #e5e7eb !important;
|
|
border-top-left-radius: 8px !important;
|
|
border-top-right-radius: 8px !important;
|
|
align-items: center !important;
|
|
}
|
|
|
|
#visual-editor-toolbar button {
|
|
/* Override global button styles */
|
|
width: auto !important;
|
|
min-width: 36px !important;
|
|
max-width: fit-content !important;
|
|
height: 36px !important;
|
|
padding: 0.4rem 0.6rem !important;
|
|
font-size: 0.875rem !important;
|
|
border: 1px solid #d1d5db !important;
|
|
background: white !important;
|
|
color: #1f2937 !important;
|
|
border-radius: 6px !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.2s !important;
|
|
margin: 0 !important;
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
white-space: nowrap !important;
|
|
transform: none !important;
|
|
box-shadow: none !important;
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
#visual-editor-toolbar button:hover {
|
|
background: #f3f4f6 !important;
|
|
border-color: #9ca3af !important;
|
|
color: #111827 !important;
|
|
}
|
|
|
|
#visual-editor-toolbar button.active {
|
|
background: #3b82f6 !important;
|
|
color: white !important;
|
|
border-color: #3b82f6 !important;
|
|
}
|
|
|
|
#visual-editor-toolbar .separator {
|
|
width: 1px !important;
|
|
height: 24px !important;
|
|
background: #d1d5db !important;
|
|
margin: 0 0.25rem !important;
|
|
display: inline-block !important;
|
|
}
|
|
|
|
/* Stili speciali per bottoni colore */
|
|
#visual-editor-toolbar button[title*="Colore testo"] {
|
|
font-weight: bold !important;
|
|
color: #dc2626 !important;
|
|
}
|
|
|
|
#visual-editor-toolbar button[title*="Colore sfondo"] {
|
|
background: linear-gradient(
|
|
135deg,
|
|
#fbbf24 0%,
|
|
#f59e0b 100%
|
|
) !important;
|
|
}
|
|
|
|
#visual-editor-content {
|
|
min-height: 500px;
|
|
padding: 1.5rem;
|
|
outline: none;
|
|
overflow-y: auto;
|
|
max-height: 800px;
|
|
}
|
|
|
|
#visual-editor-content:focus {
|
|
background: #fafafa;
|
|
}
|
|
|
|
/* Preserva tutti gli stili nell'editor */
|
|
#visual-editor-content * {
|
|
all: revert;
|
|
}
|
|
|
|
/* Stili per Quill Editor (legacy) */
|
|
#quill-editor {
|
|
background: white;
|
|
min-height: 400px;
|
|
}
|
|
|
|
.ql-toolbar {
|
|
background: #f8f9fa;
|
|
border-top-left-radius: 8px;
|
|
border-top-right-radius: 8px;
|
|
}
|
|
|
|
.ql-container {
|
|
font-size: 14px;
|
|
border-bottom-left-radius: 8px;
|
|
border-bottom-right-radius: 8px;
|
|
}
|
|
|
|
/* Stili per Monaco Editor Container */
|
|
#monaco-editor-container {
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
#monaco-editor {
|
|
width: 100%;
|
|
height: 600px;
|
|
min-height: 400px;
|
|
}
|
|
|
|
/* Stili per HTML Editor Toolbar */
|
|
.html-editor-toolbar {
|
|
display: flex !important;
|
|
flex-direction: row !important;
|
|
flex-wrap: wrap !important;
|
|
gap: 0.5rem !important;
|
|
padding: 0.75rem !important;
|
|
background: #f8f9fa !important;
|
|
border-bottom: 1px solid #e5e7eb !important;
|
|
align-items: center !important;
|
|
}
|
|
|
|
.html-editor-toolbar button {
|
|
width: auto !important;
|
|
min-width: 36px !important;
|
|
max-width: fit-content !important;
|
|
height: 36px !important;
|
|
padding: 0.4rem 0.6rem !important;
|
|
font-size: 0.875rem !important;
|
|
border: 1px solid #d1d5db !important;
|
|
background: white !important;
|
|
color: #1f2937 !important;
|
|
border-radius: 6px !important;
|
|
cursor: pointer !important;
|
|
transition: all 0.2s !important;
|
|
margin: 0 !important;
|
|
display: inline-flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
white-space: nowrap !important;
|
|
transform: none !important;
|
|
box-shadow: none !important;
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
.html-editor-toolbar button:hover {
|
|
background: #f3f4f6 !important;
|
|
border-color: #9ca3af !important;
|
|
color: #111827 !important;
|
|
}
|
|
|
|
.html-editor-toolbar .separator {
|
|
width: 1px !important;
|
|
height: 24px !important;
|
|
background: #d1d5db !important;
|
|
margin: 0 0.25rem !important;
|
|
display: inline-block !important;
|
|
}
|
|
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.action-buttons button {
|
|
width: 100%;
|
|
min-width: 100%;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
body {
|
|
background: white;
|
|
padding: 0;
|
|
}
|
|
.upload-container {
|
|
display: none !important;
|
|
}
|
|
.action-buttons,
|
|
.message {
|
|
display: none !important;
|
|
}
|
|
.result-container {
|
|
box-shadow: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
max-width: 100%;
|
|
}
|
|
#resultContent {
|
|
padding: 0;
|
|
background: white;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Sezione Upload -->
|
|
<div id="uploadSection" class="upload-container">
|
|
<h1>🎙️ Carica Audio o Trascrizione</h1>
|
|
<div class="subtitle">
|
|
Audio: MP3, WAV, M4A, OGG | Trascrizione: TXT
|
|
</div>
|
|
|
|
<form id="uploadForm">
|
|
<div class="form-group">
|
|
<label for="username">Username</label>
|
|
<input
|
|
type="text"
|
|
id="username"
|
|
name="username"
|
|
required
|
|
placeholder="Inserisci username"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Password</label>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
name="password"
|
|
required
|
|
placeholder="Inserisci password"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>File Audio</label>
|
|
<div class="file-input-wrapper">
|
|
<input
|
|
type="file"
|
|
id="fileInput"
|
|
name="file"
|
|
accept="audio/*"
|
|
/>
|
|
<label
|
|
for="fileInput"
|
|
class="file-input-label"
|
|
id="fileLabel"
|
|
>
|
|
<div class="file-info" id="fileInfo">
|
|
<div class="upload-icon">🎵</div>
|
|
<div>Tocca per selezionare un file audio</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Oppure Trascrizione (file .txt)</label>
|
|
<div class="file-input-wrapper">
|
|
<input
|
|
type="file"
|
|
id="transcriptInput"
|
|
name="transcript"
|
|
accept=".txt,text/plain"
|
|
/>
|
|
<label
|
|
for="transcriptInput"
|
|
class="file-input-label"
|
|
id="transcriptLabel"
|
|
>
|
|
<div class="file-info" id="transcriptInfo">
|
|
<div class="upload-icon">📝</div>
|
|
<div>
|
|
Tocca per selezionare una trascrizione
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Oppure Incolla Trascrizione</label>
|
|
<textarea
|
|
id="transcriptText"
|
|
name="transcriptText"
|
|
placeholder="Incolla qui la trascrizione del meeting..."
|
|
rows="8"
|
|
></textarea>
|
|
</div>
|
|
|
|
<button type="submit" id="submitBtn">Invia</button>
|
|
</form>
|
|
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
<p style="margin-top: 10px; color: #666" id="loadingText">
|
|
Caricamento in corso...
|
|
</p>
|
|
</div>
|
|
|
|
<div class="message" id="uploadMessage"></div>
|
|
</div>
|
|
|
|
<!-- Sezione Risultato -->
|
|
<div id="resultSection" class="result-container hidden">
|
|
<div class="action-buttons">
|
|
<button id="editBtn" class="btn-secondary">
|
|
✏️ Modifica Visuale
|
|
</button>
|
|
<button id="editHtmlBtn" class="btn-secondary">
|
|
🔧 Modifica HTML
|
|
</button>
|
|
<button id="printBtn" class="btn-secondary">
|
|
🖨️ Stampa/Salva PDF
|
|
</button>
|
|
<button id="downloadPdfBtn" class="btn-secondary">
|
|
📥 Genera PDF
|
|
</button>
|
|
<button id="downloadHtmlBtn" class="btn-tertiary">
|
|
📄 Scarica HTML
|
|
</button>
|
|
<button id="newUploadBtn" class="btn-tertiary">
|
|
🔄 Nuovo Upload
|
|
</button>
|
|
</div>
|
|
|
|
<div class="info-box" style="font-size: 12px">
|
|
💡 <strong>Editor Avanzato</strong>: •
|
|
<strong>Modifica Visuale</strong> - Editor WYSIWYG che preserva
|
|
TUTTI gli stili CSS e HTML dell'API •
|
|
<strong>Modifica HTML</strong> - Modifica diretta del codice con
|
|
formattazione, validazione e anteprima •
|
|
<strong>Genera PDF</strong> - Crea PDF dal contenuto modificato
|
|
• <strong>Stampa/Salva PDF</strong> - Metodo alternativo più
|
|
affidabile (seleziona "Salva come PDF" nella finestra di stampa)
|
|
</div>
|
|
|
|
<div class="message" id="resultMessage"></div>
|
|
|
|
<!-- Contenuto HTML renderizzato direttamente qui -->
|
|
<div id="resultContent"></div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/lamejs/1.2.0/lame.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
|
|
<!-- Monaco Editor - Editor di codice come VS Code -->
|
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
|
|
|
<!-- Editor WYSIWYG nativo con ContentEditable - Non serve più Quill.js -->
|
|
<script>
|
|
// Attendi il caricamento del DOM
|
|
let domReady = false;
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
domReady = true;
|
|
console.log("✅ DOM pronto");
|
|
});
|
|
} else {
|
|
domReady = true;
|
|
}
|
|
|
|
const uploadSection = document.getElementById("uploadSection");
|
|
const resultSection = document.getElementById("resultSection");
|
|
const form = document.getElementById("uploadForm");
|
|
const fileInput = document.getElementById("fileInput");
|
|
const fileLabel = document.getElementById("fileLabel");
|
|
const fileInfo = document.getElementById("fileInfo");
|
|
const transcriptInput = document.getElementById("transcriptInput");
|
|
const transcriptLabel = document.getElementById("transcriptLabel");
|
|
const transcriptInfo = document.getElementById("transcriptInfo");
|
|
const transcriptText = document.getElementById("transcriptText");
|
|
const submitBtn = document.getElementById("submitBtn");
|
|
const uploadMessage = document.getElementById("uploadMessage");
|
|
const resultMessage = document.getElementById("resultMessage");
|
|
const loading = document.getElementById("loading");
|
|
const loadingText = document.getElementById("loadingText");
|
|
const resultContent = document.getElementById("resultContent");
|
|
const editBtn = document.getElementById("editBtn");
|
|
const editHtmlBtn = document.getElementById("editHtmlBtn");
|
|
const printBtn = document.getElementById("printBtn");
|
|
const downloadPdfBtn = document.getElementById("downloadPdfBtn");
|
|
const downloadHtmlBtn = document.getElementById("downloadHtmlBtn");
|
|
const newUploadBtn = document.getElementById("newUploadBtn");
|
|
|
|
let selectedFile = null;
|
|
let selectedTranscript = null;
|
|
let receivedHtml = null;
|
|
let editorInstance = null;
|
|
let isEditing = false;
|
|
let isEditingHtml = false;
|
|
|
|
// Gestione selezione file audio
|
|
fileInput.addEventListener("change", function () {
|
|
if (this.files && this.files[0]) {
|
|
selectedFile = this.files[0];
|
|
selectedTranscript = null;
|
|
transcriptInput.value = "";
|
|
transcriptText.value = "";
|
|
transcriptLabel.classList.remove("has-file");
|
|
transcriptInfo.classList.remove("has-file");
|
|
transcriptInfo.innerHTML = `
|
|
<div class="upload-icon">📝</div>
|
|
<div>Tocca per selezionare una trascrizione</div>
|
|
`;
|
|
|
|
const fileName = selectedFile.name;
|
|
const fileSize = (selectedFile.size / 1024 / 1024).toFixed(
|
|
2,
|
|
);
|
|
const fileExt = fileName.split(".").pop().toLowerCase();
|
|
|
|
fileLabel.classList.add("has-file");
|
|
fileInfo.classList.add("has-file");
|
|
fileInfo.innerHTML = `
|
|
<div class="upload-icon">✅</div>
|
|
<div><strong>${fileName}</strong></div>
|
|
<div style="font-size: 12px; margin-top: 5px;">${fileSize} MB</div>
|
|
`;
|
|
|
|
if (fileExt === "ogg" || fileExt === "opus") {
|
|
showUploadMessage(
|
|
"⚠️ File OGG rilevato. Verrà convertito in MP3 automaticamente.",
|
|
"warning",
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Gestione selezione trascrizione da file
|
|
transcriptInput.addEventListener("change", function () {
|
|
if (this.files && this.files[0]) {
|
|
selectedTranscript = this.files[0];
|
|
selectedFile = null;
|
|
fileInput.value = "";
|
|
transcriptText.value = "";
|
|
fileLabel.classList.remove("has-file");
|
|
fileInfo.classList.remove("has-file");
|
|
fileInfo.innerHTML = `
|
|
<div class="upload-icon">🎵</div>
|
|
<div>Tocca per selezionare un file audio</div>
|
|
`;
|
|
|
|
const fileName = selectedTranscript.name;
|
|
const fileSize = (selectedTranscript.size / 1024).toFixed(
|
|
2,
|
|
);
|
|
|
|
transcriptLabel.classList.add("has-file");
|
|
transcriptInfo.classList.add("has-file");
|
|
transcriptInfo.innerHTML = `
|
|
<div class="upload-icon">✅</div>
|
|
<div><strong>${fileName}</strong></div>
|
|
<div style="font-size: 12px; margin-top: 5px;">${fileSize} KB</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Gestione textarea trascrizione
|
|
transcriptText.addEventListener("input", function () {
|
|
if (this.value.trim().length > 0) {
|
|
selectedFile = null;
|
|
selectedTranscript = null;
|
|
fileInput.value = "";
|
|
transcriptInput.value = "";
|
|
fileLabel.classList.remove("has-file");
|
|
fileInfo.classList.remove("has-file");
|
|
transcriptLabel.classList.remove("has-file");
|
|
transcriptInfo.classList.remove("has-file");
|
|
fileInfo.innerHTML = `
|
|
<div class="upload-icon">🎵</div>
|
|
<div>Tocca per selezionare un file audio</div>
|
|
`;
|
|
transcriptInfo.innerHTML = `
|
|
<div class="upload-icon">📝</div>
|
|
<div>Tocca per selezionare una trascrizione</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Funzione per convertire OGG in MP3
|
|
async function convertToMP3(audioFile) {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const arrayBuffer = await audioFile.arrayBuffer();
|
|
const audioContext = new (window.AudioContext ||
|
|
window.webkitAudioContext)();
|
|
const audioBuffer =
|
|
await audioContext.decodeAudioData(arrayBuffer);
|
|
const samples = audioBuffer.getChannelData(0);
|
|
const sampleRate = audioBuffer.sampleRate;
|
|
|
|
const pcmData = new Int16Array(samples.length);
|
|
for (let i = 0; i < samples.length; i++) {
|
|
pcmData[i] = Math.max(
|
|
-32768,
|
|
Math.min(32767, samples[i] * 32768),
|
|
);
|
|
}
|
|
|
|
const mp3encoder = new lamejs.Mp3Encoder(
|
|
1,
|
|
sampleRate,
|
|
128,
|
|
);
|
|
const mp3Data = [];
|
|
const blockSize = 1152;
|
|
|
|
for (let i = 0; i < pcmData.length; i += blockSize) {
|
|
const chunk = pcmData.subarray(i, i + blockSize);
|
|
const mp3buf = mp3encoder.encodeBuffer(chunk);
|
|
if (mp3buf.length > 0) {
|
|
mp3Data.push(mp3buf);
|
|
}
|
|
}
|
|
|
|
const mp3buf = mp3encoder.flush();
|
|
if (mp3buf.length > 0) {
|
|
mp3Data.push(mp3buf);
|
|
}
|
|
|
|
const mp3Blob = new Blob(mp3Data, {
|
|
type: "audio/mp3",
|
|
});
|
|
const mp3File = new File(
|
|
[mp3Blob],
|
|
audioFile.name.replace(/\.[^/.]+$/, ".mp3"),
|
|
{
|
|
type: "audio/mp3",
|
|
},
|
|
);
|
|
|
|
resolve(mp3File);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Gestione invio form
|
|
form.addEventListener("submit", async function (e) {
|
|
e.preventDefault();
|
|
|
|
const username = document.getElementById("username").value;
|
|
const password = document.getElementById("password").value;
|
|
const pastedTranscript = transcriptText.value.trim();
|
|
|
|
if (!selectedFile && !selectedTranscript && !pastedTranscript) {
|
|
showUploadMessage(
|
|
"Seleziona un file audio, carica una trascrizione o incolla il testo",
|
|
"error",
|
|
);
|
|
return;
|
|
}
|
|
|
|
submitBtn.disabled = true;
|
|
loading.classList.add("show");
|
|
uploadMessage.classList.remove("show");
|
|
|
|
try {
|
|
const credentials = btoa(`${username}:${password}`);
|
|
let requestBody;
|
|
let requestHeaders = {
|
|
Authorization: `Basic ${credentials}`,
|
|
};
|
|
|
|
// Se è stato incollato del testo nella textarea o caricato un file di trascrizione
|
|
if (pastedTranscript || selectedTranscript) {
|
|
loadingText.textContent =
|
|
"Elaborazione trascrizione...";
|
|
|
|
// Ottieni il testo (da textarea o da file)
|
|
let transcriptTextContent;
|
|
if (pastedTranscript) {
|
|
transcriptTextContent = pastedTranscript;
|
|
} else {
|
|
transcriptTextContent =
|
|
await selectedTranscript.text();
|
|
}
|
|
|
|
// Invia come body text/plain
|
|
requestBody = transcriptTextContent;
|
|
requestHeaders["Content-Type"] = "text/plain";
|
|
}
|
|
// Altrimenti gestisci il file audio con FormData
|
|
else {
|
|
let fileToUpload = selectedFile;
|
|
const fileExt = selectedFile.name
|
|
.split(".")
|
|
.pop()
|
|
.toLowerCase();
|
|
|
|
if (fileExt === "ogg" || fileExt === "opus") {
|
|
loadingText.textContent = "Conversione in MP3...";
|
|
fileLabel.classList.add("converting");
|
|
fileInfo.classList.add("converting");
|
|
|
|
try {
|
|
fileToUpload = await convertToMP3(selectedFile);
|
|
showUploadMessage(
|
|
"✓ File convertito in MP3",
|
|
"success",
|
|
);
|
|
|
|
const newSize = (
|
|
fileToUpload.size /
|
|
1024 /
|
|
1024
|
|
).toFixed(2);
|
|
fileInfo.innerHTML = `
|
|
<div class="upload-icon">🔄</div>
|
|
<div><strong>${fileToUpload.name}</strong></div>
|
|
<div style="font-size: 12px; margin-top: 5px;">${newSize} MB (convertito)</div>
|
|
`;
|
|
|
|
await new Promise((resolve) =>
|
|
setTimeout(resolve, 1000),
|
|
);
|
|
} catch (convError) {
|
|
console.error("Conversion error:", convError);
|
|
showUploadMessage(
|
|
"⚠️ Impossibile convertire. Invio file originale...",
|
|
"warning",
|
|
);
|
|
await new Promise((resolve) =>
|
|
setTimeout(resolve, 1500),
|
|
);
|
|
}
|
|
|
|
fileLabel.classList.remove("converting");
|
|
fileInfo.classList.remove("converting");
|
|
}
|
|
|
|
loadingText.textContent = "Elaborazione audio...";
|
|
const formData = new FormData();
|
|
formData.append("file", fileToUpload);
|
|
requestBody = formData;
|
|
}
|
|
|
|
// Create fetch with timeout for Firefox
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(
|
|
() => controller.abort(),
|
|
300000,
|
|
); // 5 minutes timeout
|
|
|
|
const response = await fetch(
|
|
"https://n8n-prod.commandware.com/webhook/ai_meeting_parse",
|
|
{
|
|
method: "POST",
|
|
headers: requestHeaders,
|
|
body: requestBody,
|
|
signal: controller.signal,
|
|
},
|
|
);
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (response.ok) {
|
|
const contentType =
|
|
response.headers.get("content-type");
|
|
|
|
if (contentType && contentType.includes("text/html")) {
|
|
receivedHtml = await response.text();
|
|
displayResult(receivedHtml);
|
|
} else {
|
|
const result = await response.text();
|
|
try {
|
|
const jsonResult = JSON.parse(result);
|
|
if (jsonResult.html) {
|
|
receivedHtml = jsonResult.html;
|
|
displayResult(receivedHtml);
|
|
} else {
|
|
showUploadMessage(
|
|
"✅ File processato con successo!",
|
|
"success",
|
|
);
|
|
}
|
|
} catch {
|
|
receivedHtml = result;
|
|
displayResult(receivedHtml);
|
|
}
|
|
}
|
|
} else {
|
|
const errorText = await response.text();
|
|
showUploadMessage(
|
|
`❌ Errore: ${response.status} - ${errorText || response.statusText}`,
|
|
"error",
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Upload error:", error);
|
|
showUploadMessage(
|
|
`❌ Errore di connessione: ${error.message}`,
|
|
"error",
|
|
);
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
loading.classList.remove("show");
|
|
loadingText.textContent = "Caricamento in corso...";
|
|
}
|
|
});
|
|
|
|
function displayResult(html) {
|
|
// Disattiva l'editor se era attivo
|
|
if (isEditing) {
|
|
destroyEditor();
|
|
editBtn.innerHTML = "✏️ Modifica";
|
|
editBtn.classList.remove("btn-tertiary");
|
|
editBtn.classList.add("btn-secondary");
|
|
isEditing = false;
|
|
}
|
|
|
|
uploadSection.classList.add("hidden");
|
|
resultSection.classList.remove("hidden");
|
|
resultContent.innerHTML = html;
|
|
resultSection.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "start",
|
|
});
|
|
|
|
const scripts = resultContent.querySelectorAll("script");
|
|
scripts.forEach((oldScript) => {
|
|
const newScript = document.createElement("script");
|
|
Array.from(oldScript.attributes).forEach((attr) => {
|
|
newScript.setAttribute(attr.name, attr.value);
|
|
});
|
|
newScript.textContent = oldScript.textContent;
|
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
|
});
|
|
}
|
|
|
|
// Stampa diretta
|
|
printBtn.addEventListener("click", function () {
|
|
// Se l'editor è attivo, salva le modifiche prima di stampare
|
|
if (isEditing && editorInstance) {
|
|
receivedHtml = editorInstance.root.innerHTML;
|
|
destroyEditor();
|
|
resultContent.innerHTML = receivedHtml;
|
|
editBtn.innerHTML = "✏️ Modifica";
|
|
editBtn.classList.remove("btn-tertiary");
|
|
editBtn.classList.add("btn-secondary");
|
|
isEditing = false;
|
|
}
|
|
|
|
const actionButtons = document.querySelector(".action-buttons");
|
|
const messages = document.querySelectorAll(".message");
|
|
|
|
actionButtons.style.display = "none";
|
|
messages.forEach((msg) => (msg.style.display = "none"));
|
|
|
|
window.print();
|
|
|
|
setTimeout(() => {
|
|
actionButtons.style.display = "flex";
|
|
}, 100);
|
|
});
|
|
|
|
// Download PDF - Costruzione programmatica con jsPDF
|
|
downloadPdfBtn.addEventListener("click", function () {
|
|
if (!receivedHtml) return;
|
|
|
|
// Ottieni il contenuto attuale (dall'editor se attivo, altrimenti usa receivedHtml)
|
|
let contentToProcess = receivedHtml;
|
|
if (isEditing && editorInstance) {
|
|
contentToProcess = editorInstance.root.innerHTML;
|
|
}
|
|
|
|
downloadPdfBtn.disabled = true;
|
|
downloadPdfBtn.textContent = "⏳ Generazione PDF...";
|
|
|
|
try {
|
|
const { jsPDF } = window.jspdf;
|
|
const doc = new jsPDF();
|
|
|
|
// Parse HTML per estrarre dati
|
|
const parser = new DOMParser();
|
|
const htmlDoc = parser.parseFromString(
|
|
contentToProcess,
|
|
"text/html",
|
|
);
|
|
|
|
// Estrai titolo
|
|
const titolo =
|
|
htmlDoc.querySelector("h1")?.textContent ||
|
|
"Verbale di Riunione";
|
|
|
|
// Estrai metadata
|
|
const metadataItems =
|
|
htmlDoc.querySelectorAll(".metadata-item");
|
|
let data = "",
|
|
ora = "",
|
|
luogo = "",
|
|
organizzatore = "";
|
|
metadataItems.forEach((item) => {
|
|
const label = item
|
|
.querySelector(".metadata-label")
|
|
?.textContent.toLowerCase();
|
|
const value =
|
|
item.querySelector(".metadata-value")
|
|
?.textContent || "";
|
|
if (label?.includes("data")) data = value;
|
|
if (label?.includes("ora")) ora = value;
|
|
if (label?.includes("luogo")) luogo = value;
|
|
if (label?.includes("organizzatore"))
|
|
organizzatore = value;
|
|
});
|
|
|
|
// Estrai partecipanti
|
|
const partecipanti = Array.from(
|
|
htmlDoc.querySelectorAll(".participant"),
|
|
).map((p) => p.textContent);
|
|
|
|
// Estrai sezioni
|
|
const sezioni = htmlDoc.querySelectorAll(".section");
|
|
let obiettivo = "",
|
|
argomenti = [],
|
|
decisioni = [],
|
|
azioni = [],
|
|
note = "",
|
|
prossimo = "";
|
|
|
|
sezioni.forEach((sezione) => {
|
|
const titolo =
|
|
sezione.querySelector(
|
|
".section-title",
|
|
)?.textContent;
|
|
if (titolo?.includes("Obiettivo")) {
|
|
obiettivo =
|
|
sezione.querySelector(".content-text")
|
|
?.textContent || "";
|
|
} else if (titolo?.includes("Argomenti")) {
|
|
argomenti = Array.from(
|
|
sezione.querySelectorAll(".topic-item"),
|
|
).map((t) => ({
|
|
titolo:
|
|
t.querySelector(".topic-title")
|
|
?.textContent || "",
|
|
desc:
|
|
t.querySelector(".topic-description")
|
|
?.textContent || "",
|
|
}));
|
|
} else if (titolo?.includes("Decisioni")) {
|
|
decisioni = Array.from(
|
|
sezione.querySelectorAll(".decision-item"),
|
|
).map((d) => d.textContent);
|
|
} else if (titolo?.includes("Azioni")) {
|
|
azioni = Array.from(
|
|
sezione.querySelectorAll(".action-item"),
|
|
).map((a) => ({
|
|
desc:
|
|
a.querySelector(".action-description")
|
|
?.textContent || "",
|
|
assegnato:
|
|
a.querySelector(".action-assignee")
|
|
?.textContent || "",
|
|
scadenza:
|
|
a.querySelector(".action-deadline")
|
|
?.textContent || "",
|
|
}));
|
|
} else if (titolo?.includes("Note")) {
|
|
note =
|
|
sezione.querySelector(".content-text")
|
|
?.textContent || "";
|
|
} else if (titolo?.includes("Prossimo")) {
|
|
prossimo =
|
|
sezione.querySelector(".content-text")
|
|
?.textContent || "";
|
|
}
|
|
});
|
|
|
|
let yPos = 20;
|
|
|
|
// === HEADER ===
|
|
doc.setFillColor(44, 62, 80);
|
|
doc.rect(0, 0, 210, 45, "F");
|
|
|
|
doc.setFontSize(24);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.text(titolo.substring(0, 50), 105, 25, {
|
|
align: "center",
|
|
});
|
|
|
|
doc.setFontSize(10);
|
|
doc.setFont(undefined, "normal");
|
|
doc.text(data, 105, 35, { align: "center" });
|
|
|
|
yPos = 55;
|
|
|
|
// === METADATA ===
|
|
doc.setFillColor(248, 249, 250);
|
|
doc.roundedRect(15, yPos, 180, 30, 3, 3, "F");
|
|
doc.setDrawColor(224, 224, 224);
|
|
doc.setLineWidth(0.5);
|
|
doc.roundedRect(15, yPos, 180, 30, 3, 3, "S");
|
|
|
|
doc.setFontSize(9);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(85, 85, 85);
|
|
|
|
doc.text("Data:", 20, yPos + 8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(51, 51, 51);
|
|
doc.text(data, 40, yPos + 8);
|
|
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(85, 85, 85);
|
|
doc.text("Ora:", 110, yPos + 8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(51, 51, 51);
|
|
doc.text(ora, 125, yPos + 8);
|
|
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(85, 85, 85);
|
|
doc.text("Luogo:", 20, yPos + 16);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(51, 51, 51);
|
|
doc.text(luogo.substring(0, 35), 40, yPos + 16);
|
|
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(85, 85, 85);
|
|
doc.text("Organizzatore:", 20, yPos + 24);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(51, 51, 51);
|
|
doc.text(organizzatore, 55, yPos + 24);
|
|
|
|
yPos += 38;
|
|
|
|
// === PARTECIPANTI ===
|
|
doc.setFontSize(12);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text("Partecipanti", 15, yPos);
|
|
yPos += 8;
|
|
|
|
doc.setFontSize(8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(51, 51, 51);
|
|
let xPos = 15;
|
|
partecipanti.forEach((p, idx) => {
|
|
if (xPos > 170) {
|
|
xPos = 15;
|
|
yPos += 8;
|
|
}
|
|
doc.setFillColor(232, 244, 248);
|
|
doc.roundedRect(
|
|
xPos,
|
|
yPos - 5,
|
|
doc.getTextWidth(p) + 6,
|
|
7,
|
|
2,
|
|
2,
|
|
"F",
|
|
);
|
|
doc.text(p, xPos + 3, yPos);
|
|
xPos += doc.getTextWidth(p) + 10;
|
|
});
|
|
|
|
yPos += 12;
|
|
|
|
// === OBIETTIVO ===
|
|
if (obiettivo) {
|
|
doc.setFontSize(12);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text("Obiettivo della Riunione", 15, yPos);
|
|
yPos += 8;
|
|
|
|
doc.setFontSize(9);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(68, 68, 68);
|
|
const obiettivoLines = doc.splitTextToSize(
|
|
obiettivo,
|
|
170,
|
|
);
|
|
doc.text(obiettivoLines, 15, yPos);
|
|
yPos += obiettivoLines.length * 5 + 5;
|
|
}
|
|
|
|
// === ARGOMENTI ===
|
|
if (argomenti.length > 0) {
|
|
if (yPos > 250) {
|
|
doc.addPage();
|
|
yPos = 20;
|
|
}
|
|
|
|
doc.setFontSize(12);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text("Argomenti Discussi", 15, yPos);
|
|
yPos += 8;
|
|
|
|
argomenti.forEach((arg) => {
|
|
if (yPos > 270) {
|
|
doc.addPage();
|
|
yPos = 20;
|
|
}
|
|
|
|
doc.setFillColor(250, 250, 250);
|
|
doc.setDrawColor(52, 152, 219);
|
|
doc.setLineWidth(2);
|
|
doc.line(15, yPos - 3, 15, yPos + 12);
|
|
doc.roundedRect(18, yPos - 5, 177, 18, 2, 2, "F");
|
|
|
|
doc.setFontSize(10);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text(arg.titolo.substring(0, 60), 20, yPos);
|
|
|
|
doc.setFontSize(8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(85, 85, 85);
|
|
const descLines = doc.splitTextToSize(
|
|
arg.desc.substring(0, 200),
|
|
170,
|
|
);
|
|
doc.text(descLines, 20, yPos + 5);
|
|
|
|
yPos += Math.max(20, descLines.length * 4 + 8);
|
|
});
|
|
}
|
|
|
|
// === DECISIONI ===
|
|
if (decisioni.length > 0) {
|
|
if (yPos > 250) {
|
|
doc.addPage();
|
|
yPos = 20;
|
|
}
|
|
|
|
doc.setFontSize(12);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text("Decisioni Prese", 15, yPos);
|
|
yPos += 8;
|
|
|
|
decisioni.forEach((dec) => {
|
|
if (yPos > 275) {
|
|
doc.addPage();
|
|
yPos = 20;
|
|
}
|
|
|
|
doc.setFillColor(232, 245, 233);
|
|
doc.setDrawColor(76, 175, 80);
|
|
doc.setLineWidth(2);
|
|
doc.line(15, yPos - 3, 15, yPos + 7);
|
|
doc.roundedRect(18, yPos - 5, 177, 12, 2, 2, "F");
|
|
|
|
doc.setFontSize(8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(51, 51, 51);
|
|
const decLines = doc.splitTextToSize(
|
|
dec.substring(0, 150),
|
|
170,
|
|
);
|
|
doc.text(decLines, 20, yPos);
|
|
|
|
yPos += Math.max(12, decLines.length * 4 + 3);
|
|
});
|
|
}
|
|
|
|
// === AZIONI ===
|
|
if (azioni.length > 0) {
|
|
if (yPos > 230) {
|
|
doc.addPage();
|
|
yPos = 20;
|
|
}
|
|
|
|
doc.setFontSize(12);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text("Azioni da Intraprendere", 15, yPos);
|
|
yPos += 8;
|
|
|
|
azioni.forEach((az) => {
|
|
if (yPos > 265) {
|
|
doc.addPage();
|
|
yPos = 20;
|
|
}
|
|
|
|
doc.setFillColor(255, 243, 224);
|
|
doc.setDrawColor(255, 152, 0);
|
|
doc.setLineWidth(2);
|
|
doc.line(15, yPos - 3, 15, yPos + 12);
|
|
doc.roundedRect(18, yPos - 5, 177, 18, 2, 2, "F");
|
|
|
|
doc.setFontSize(8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(85, 85, 85);
|
|
const azLines = doc.splitTextToSize(
|
|
az.desc.substring(0, 120),
|
|
120,
|
|
);
|
|
doc.text(azLines, 20, yPos);
|
|
|
|
if (az.assegnato) {
|
|
doc.setFillColor(255, 152, 0);
|
|
doc.roundedRect(
|
|
145,
|
|
yPos - 3,
|
|
25,
|
|
6,
|
|
2,
|
|
2,
|
|
"F",
|
|
);
|
|
doc.setFontSize(7);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.text(
|
|
az.assegnato.substring(0, 12),
|
|
157.5,
|
|
yPos + 1,
|
|
{ align: "center" },
|
|
);
|
|
}
|
|
|
|
if (az.scadenza) {
|
|
doc.setDrawColor(255, 152, 0);
|
|
doc.setLineWidth(0.5);
|
|
doc.roundedRect(
|
|
172,
|
|
yPos - 3,
|
|
23,
|
|
6,
|
|
2,
|
|
2,
|
|
"S",
|
|
);
|
|
doc.setFontSize(7);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(255, 152, 0);
|
|
doc.text(
|
|
az.scadenza.substring(0, 12),
|
|
183.5,
|
|
yPos + 1,
|
|
{ align: "center" },
|
|
);
|
|
}
|
|
|
|
yPos += Math.max(20, azLines.length * 4 + 8);
|
|
});
|
|
}
|
|
|
|
// === NOTE ===
|
|
if (note && yPos < 270) {
|
|
doc.setFontSize(12);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text("Note Aggiuntive", 15, yPos);
|
|
yPos += 8;
|
|
|
|
doc.setFontSize(8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(68, 68, 68);
|
|
const noteLines = doc.splitTextToSize(note, 170);
|
|
doc.text(noteLines, 15, yPos);
|
|
yPos += noteLines.length * 4 + 5;
|
|
}
|
|
|
|
// === PROSSIMO MEETING ===
|
|
if (prossimo && yPos < 270) {
|
|
doc.setFontSize(12);
|
|
doc.setFont(undefined, "bold");
|
|
doc.setTextColor(44, 62, 80);
|
|
doc.text("Prossimo Incontro", 15, yPos);
|
|
yPos += 8;
|
|
|
|
doc.setFontSize(8);
|
|
doc.setFont(undefined, "normal");
|
|
doc.setTextColor(68, 68, 68);
|
|
doc.text(prossimo, 15, yPos);
|
|
}
|
|
|
|
// === FOOTER ===
|
|
const footer =
|
|
htmlDoc.querySelector(".footer")?.textContent || "";
|
|
doc.setFontSize(7);
|
|
doc.setFont(undefined, "italic");
|
|
doc.setTextColor(136, 136, 136);
|
|
doc.text(footer, 105, 285, { align: "center" });
|
|
|
|
// Salva PDF
|
|
const filename = `verbale_${new Date().getTime()}.pdf`;
|
|
doc.save(filename);
|
|
|
|
showResultMessage(
|
|
"✅ PDF generato con successo!",
|
|
"success",
|
|
);
|
|
} catch (error) {
|
|
console.error("PDF generation error:", error);
|
|
showResultMessage("❌ Errore: " + error.message, "error");
|
|
} finally {
|
|
downloadPdfBtn.disabled = false;
|
|
downloadPdfBtn.textContent = "📥 Genera PDF";
|
|
}
|
|
});
|
|
|
|
// Download HTML
|
|
downloadHtmlBtn.addEventListener("click", function () {
|
|
if (!receivedHtml) return;
|
|
|
|
// Ottieni il contenuto attuale (dall'editor se attivo, altrimenti usa receivedHtml)
|
|
let contentToDownload = receivedHtml;
|
|
if (isEditing && editorInstance) {
|
|
contentToDownload = editorInstance.root.innerHTML;
|
|
}
|
|
|
|
const blob = new Blob([contentToDownload], {
|
|
type: "text/html",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `risultato_${new Date().getTime()}.html`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
showResultMessage("✅ HTML scaricato con successo!", "success");
|
|
});
|
|
|
|
// Funzioni per gestire l'editor WYSIWYG avanzato con ContentEditable
|
|
// Preserva TUTTO l'HTML e CSS dall'API senza limitazioni
|
|
function initEditor() {
|
|
if (editorInstance) {
|
|
return; // Editor già inizializzato
|
|
}
|
|
|
|
// Salva il contenuto HTML corrente
|
|
const currentContent = receivedHtml || resultContent.innerHTML;
|
|
|
|
// Crea il container dell'editor visuale
|
|
const editorContainer = document.createElement("div");
|
|
editorContainer.id = "visual-editor-container";
|
|
|
|
// Crea la toolbar
|
|
const toolbar = document.createElement("div");
|
|
toolbar.id = "visual-editor-toolbar";
|
|
toolbar.innerHTML = `
|
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; width: 100%;">
|
|
<button onclick="execCmd('bold')" title="Grassetto (Ctrl+B)"><strong>B</strong></button>
|
|
<button onclick="execCmd('italic')" title="Corsivo (Ctrl+I)"><em>I</em></button>
|
|
<button onclick="execCmd('underline')" title="Sottolineato (Ctrl+U)"><u>U</u></button>
|
|
<button onclick="execCmd('strikeThrough')" title="Barrato"><s>S</s></button>
|
|
<div class="separator"></div>
|
|
<button onclick="execCmd('formatBlock', 'h1')" title="Titolo 1"><strong>H1</strong></button>
|
|
<button onclick="execCmd('formatBlock', 'h2')" title="Titolo 2"><strong>H2</strong></button>
|
|
<button onclick="execCmd('formatBlock', 'h3')" title="Titolo 3"><strong>H3</strong></button>
|
|
<button onclick="execCmd('formatBlock', 'p')" title="Paragrafo">P</button>
|
|
<div class="separator"></div>
|
|
<button onclick="execCmd('insertUnorderedList')" title="Elenco puntato">•</button>
|
|
<button onclick="execCmd('insertOrderedList')" title="Elenco numerato">1.</button>
|
|
<div class="separator"></div>
|
|
<button onclick="execCmd('justifyLeft')" title="Allinea a sinistra">⬅️</button>
|
|
<button onclick="execCmd('justifyCenter')" title="Centra">⬌</button>
|
|
<button onclick="execCmd('justifyRight')" title="Allinea a destra">➡️</button>
|
|
<div class="separator"></div>
|
|
<button onclick="insertLink()" title="Inserisci/Modifica link">🔗</button>
|
|
<button onclick="changeColor()" title="Colore testo">A</button>
|
|
<button onclick="changeBgColor()" title="Colore sfondo">◼️</button>
|
|
<div class="separator"></div>
|
|
<button onclick="execCmd('removeFormat')" title="Rimuovi formattazione">✕</button>
|
|
<button onclick="viewHtmlSource()" title="Visualizza/Modifica HTML"></></button>
|
|
</div>
|
|
`;
|
|
|
|
// Crea il contenteditable
|
|
const editorContent = document.createElement("div");
|
|
editorContent.id = "visual-editor-content";
|
|
editorContent.contentEditable = "true";
|
|
editorContent.innerHTML = currentContent;
|
|
|
|
// Assembla l'editor
|
|
editorContainer.appendChild(toolbar);
|
|
editorContainer.appendChild(editorContent);
|
|
|
|
// Sostituisci il contenuto
|
|
resultContent.innerHTML = "";
|
|
resultContent.appendChild(editorContainer);
|
|
|
|
// Salva riferimento
|
|
editorInstance = editorContent;
|
|
|
|
console.log("✅ Editor visuale avanzato inizializzato");
|
|
}
|
|
|
|
function destroyEditor() {
|
|
if (editorInstance) {
|
|
// Salva il contenuto prima di distruggere l'editor
|
|
receivedHtml = editorInstance.innerHTML;
|
|
editorInstance = null;
|
|
}
|
|
}
|
|
|
|
// Funzioni helper per l'editor visuale
|
|
function execCmd(command, value = null) {
|
|
document.execCommand(command, false, value);
|
|
document.getElementById("visual-editor-content").focus();
|
|
}
|
|
|
|
function insertLink() {
|
|
const url = prompt("Inserisci URL:", "https://");
|
|
if (url && url !== "https://") {
|
|
execCmd("createLink", url);
|
|
}
|
|
}
|
|
|
|
function changeColor() {
|
|
// Crea un input color nascosto per selezione nativa
|
|
const input = document.createElement("input");
|
|
input.type = "color";
|
|
input.value = "#000000";
|
|
input.style.position = "absolute";
|
|
input.style.opacity = "0";
|
|
input.style.pointerEvents = "none";
|
|
document.body.appendChild(input);
|
|
|
|
input.addEventListener("change", function () {
|
|
execCmd("foreColor", this.value);
|
|
document.body.removeChild(input);
|
|
});
|
|
|
|
input.click();
|
|
}
|
|
|
|
function changeBgColor() {
|
|
// Crea un input color nascosto per selezione nativa
|
|
const input = document.createElement("input");
|
|
input.type = "color";
|
|
input.value = "#ffff00";
|
|
input.style.position = "absolute";
|
|
input.style.opacity = "0";
|
|
input.style.pointerEvents = "none";
|
|
document.body.appendChild(input);
|
|
|
|
input.addEventListener("change", function () {
|
|
execCmd("backColor", this.value);
|
|
document.body.removeChild(input);
|
|
});
|
|
|
|
input.click();
|
|
}
|
|
|
|
function viewHtmlSource() {
|
|
if (!isEditingHtml) {
|
|
toggleHtmlEditor();
|
|
}
|
|
}
|
|
|
|
function toggleEditor() {
|
|
// Chiudi l'editor HTML se aperto
|
|
if (isEditingHtml) {
|
|
toggleHtmlEditor();
|
|
}
|
|
|
|
if (isEditing) {
|
|
// Disattiva modalità editing
|
|
destroyEditor();
|
|
resultContent.innerHTML = receivedHtml;
|
|
editBtn.innerHTML = "✏️ Modifica Visuale";
|
|
editBtn.classList.remove("btn-tertiary");
|
|
editBtn.classList.add("btn-secondary");
|
|
isEditing = false;
|
|
} else {
|
|
// Attiva modalità editing
|
|
initEditor();
|
|
editBtn.innerHTML = "💾 Salva Modifiche";
|
|
editBtn.classList.remove("btn-secondary");
|
|
editBtn.classList.add("btn-tertiary");
|
|
isEditing = true;
|
|
}
|
|
}
|
|
|
|
// Variabile globale per Monaco Editor
|
|
let monacoEditor = null;
|
|
|
|
// Funzioni per l'editor HTML raw con Monaco
|
|
function initHtmlEditor() {
|
|
// Salva il contenuto HTML corrente (prendi dall'editor visuale se attivo)
|
|
let currentContent;
|
|
if (editorInstance && editorInstance.innerHTML) {
|
|
currentContent = editorInstance.innerHTML;
|
|
} else {
|
|
currentContent = receivedHtml || resultContent.innerHTML;
|
|
}
|
|
|
|
// Crea l'interfaccia dell'editor HTML
|
|
resultContent.innerHTML = `
|
|
<div id="monaco-editor-container">
|
|
<div class="html-editor-toolbar">
|
|
<button onclick="formatHtml()" title="Formatta e indenta HTML"><strong>🎨</strong> Formatta</button>
|
|
<button onclick="validateHtml()" title="Valida sintassi HTML"><strong>✓</strong> Valida</button>
|
|
<button onclick="previewHtml()" title="Apri anteprima in nuova finestra"><strong>👁️</strong> Anteprima</button>
|
|
<div class="separator"></div>
|
|
<button onclick="monacoEditor.trigger('keyboard', 'editor.action.commentLine')" title="Commenta/Decommenta (Ctrl+/)">💬</button>
|
|
<button onclick="monacoEditor.getAction('editor.foldAll').run()" title="Riduci tutto">📁</button>
|
|
<button onclick="monacoEditor.getAction('editor.unfoldAll').run()" title="Espandi tutto">📂</button>
|
|
<div class="separator"></div>
|
|
<span style="margin-left: auto; color: #6b7280; font-size: 12px;">Editor avanzato con syntax highlighting • Ctrl+Z/Y per Undo/Redo</span>
|
|
</div>
|
|
<div id="monaco-editor"></div>
|
|
</div>
|
|
`;
|
|
|
|
// Inizializza Monaco Editor
|
|
require.config({
|
|
paths: {
|
|
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs",
|
|
},
|
|
});
|
|
require(["vs/editor/editor.main"], function () {
|
|
monacoEditor = monaco.editor.create(
|
|
document.getElementById("monaco-editor"),
|
|
{
|
|
value: currentContent,
|
|
language: "html",
|
|
theme: "vs-dark",
|
|
automaticLayout: true,
|
|
minimap: { enabled: true },
|
|
fontSize: 14,
|
|
lineNumbers: "on",
|
|
roundedSelection: true,
|
|
scrollBeyondLastLine: false,
|
|
readOnly: false,
|
|
wordWrap: "on",
|
|
formatOnPaste: true,
|
|
formatOnType: true,
|
|
tabSize: 2,
|
|
insertSpaces: true,
|
|
autoIndent: "full",
|
|
folding: true,
|
|
foldingStrategy: "indentation",
|
|
showFoldingControls: "always",
|
|
matchBrackets: "always",
|
|
autoClosingBrackets: "always",
|
|
autoClosingQuotes: "always",
|
|
suggestOnTriggerCharacters: true,
|
|
quickSuggestions: true,
|
|
},
|
|
);
|
|
|
|
console.log("✅ Monaco Editor inizializzato");
|
|
});
|
|
}
|
|
|
|
function destroyHtmlEditor() {
|
|
if (monacoEditor) {
|
|
// Salva il contenuto modificato
|
|
receivedHtml = monacoEditor.getValue();
|
|
// Distruggi l'editor per liberare risorse
|
|
monacoEditor.dispose();
|
|
monacoEditor = null;
|
|
}
|
|
}
|
|
|
|
function toggleHtmlEditor() {
|
|
// Chiudi l'editor visuale se aperto
|
|
if (isEditing) {
|
|
toggleEditor();
|
|
}
|
|
|
|
if (isEditingHtml) {
|
|
// Disattiva modalità editing HTML
|
|
destroyHtmlEditor();
|
|
resultContent.innerHTML = receivedHtml;
|
|
editHtmlBtn.innerHTML = "🔧 Modifica HTML";
|
|
editHtmlBtn.classList.remove("btn-tertiary");
|
|
editHtmlBtn.classList.add("btn-secondary");
|
|
isEditingHtml = false;
|
|
} else {
|
|
// Attiva modalità editing HTML
|
|
initHtmlEditor();
|
|
editHtmlBtn.innerHTML = "💾 Salva HTML";
|
|
editHtmlBtn.classList.remove("btn-secondary");
|
|
editHtmlBtn.classList.add("btn-tertiary");
|
|
isEditingHtml = true;
|
|
}
|
|
}
|
|
|
|
// Utility per escape HTML nell'editor
|
|
function escapeHtml(html) {
|
|
const div = document.createElement("div");
|
|
div.textContent = html;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Formatta HTML con Monaco built-in formatter
|
|
function formatHtml() {
|
|
if (!monacoEditor) {
|
|
showResultMessage("error", "Editor non disponibile");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Monaco ha un formattatore integrato eccellente
|
|
monacoEditor
|
|
.getAction("editor.action.formatDocument")
|
|
.run();
|
|
showResultMessage(
|
|
"success",
|
|
"✅ HTML formattato correttamente",
|
|
);
|
|
} catch (error) {
|
|
showResultMessage(
|
|
"error",
|
|
"Errore formattazione: " + error.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Valida HTML
|
|
function validateHtml() {
|
|
if (!monacoEditor) {
|
|
showResultMessage("error", "Editor non disponibile");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const htmlContent = monacoEditor.getValue();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(
|
|
htmlContent,
|
|
"text/html",
|
|
);
|
|
const errors = doc.querySelector("parsererror");
|
|
|
|
if (errors) {
|
|
showResultMessage(
|
|
"error",
|
|
"❌ HTML non valido: " + errors.textContent,
|
|
);
|
|
} else {
|
|
showResultMessage("success", "✅ HTML valido");
|
|
}
|
|
} catch (error) {
|
|
showResultMessage(
|
|
"error",
|
|
"Errore validazione: " + error.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Anteprima HTML
|
|
function previewHtml() {
|
|
if (!monacoEditor) {
|
|
showResultMessage("error", "Editor non disponibile");
|
|
return;
|
|
}
|
|
|
|
// Salva l'HTML corrente
|
|
const htmlContent = monacoEditor.getValue();
|
|
|
|
// Crea una finestra di anteprima
|
|
const previewWindow = window.open(
|
|
"",
|
|
"Anteprima HTML",
|
|
"width=900,height=700",
|
|
);
|
|
previewWindow.document.write(htmlContent);
|
|
previewWindow.document.close();
|
|
}
|
|
|
|
// Event listeners per i pulsanti modifica
|
|
editBtn.addEventListener("click", toggleEditor);
|
|
editHtmlBtn.addEventListener("click", toggleHtmlEditor);
|
|
|
|
// Nuovo upload
|
|
newUploadBtn.addEventListener("click", function () {
|
|
// Disattiva l'editor visuale se era attivo
|
|
if (isEditing) {
|
|
destroyEditor();
|
|
editBtn.innerHTML = "✏️ Modifica Visuale";
|
|
editBtn.classList.remove("btn-tertiary");
|
|
editBtn.classList.add("btn-secondary");
|
|
isEditing = false;
|
|
}
|
|
|
|
// Disattiva l'editor HTML se era attivo
|
|
if (isEditingHtml) {
|
|
destroyHtmlEditor();
|
|
editHtmlBtn.innerHTML = "🔧 Modifica HTML";
|
|
editHtmlBtn.classList.remove("btn-tertiary");
|
|
editHtmlBtn.classList.add("btn-secondary");
|
|
isEditingHtml = false;
|
|
}
|
|
|
|
resultSection.classList.add("hidden");
|
|
uploadSection.classList.remove("hidden");
|
|
|
|
form.reset();
|
|
selectedFile = null;
|
|
receivedHtml = null;
|
|
resultContent.innerHTML = "";
|
|
|
|
fileLabel.classList.remove("has-file");
|
|
fileInfo.classList.remove("has-file");
|
|
fileInfo.innerHTML = `
|
|
<div class="upload-icon">🎵</div>
|
|
<div>Tocca per selezionare un file audio</div>
|
|
`;
|
|
|
|
uploadMessage.classList.remove("show");
|
|
resultMessage.classList.remove("show");
|
|
|
|
uploadSection.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "start",
|
|
});
|
|
});
|
|
|
|
function showUploadMessage(text, type) {
|
|
uploadMessage.textContent = text;
|
|
uploadMessage.className = `message ${type} show`;
|
|
|
|
if (type === "success" || type === "warning") {
|
|
setTimeout(() => {
|
|
uploadMessage.classList.remove("show");
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
function showResultMessage(text, type) {
|
|
resultMessage.textContent = text;
|
|
resultMessage.className = `message ${type} show`;
|
|
|
|
setTimeout(() => {
|
|
resultMessage.classList.remove("show");
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|