Files
cai-foligno-tools/ai-meeting/meeting.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">&lt;/&gt;</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>