[FEAT] ARIA voce naturale (proxy TTS nexus-voice-ms) + toggle voce — Fase C
Porting voce TRPG/AllTax (ElevenLabs "Sarah") sull'ARIA chat NIS2: - AiController::tts() + route POST /api/ai/tts: proxy same-origin a nexus-voice-ms (config VOICE_MS_URL=172.21.0.1:4215, voce TTS_VOICE_ID=EXAVITQu4vr4xnSDxMaL, eleven_turbo_v2_5). requireAuth, cache MP3 sha256 (sys_get_temp_dir/nis2-tts-cache), validazioni (text<=800, voice_id allowlist), stream audio/mpeg, 502 se upstream non-audio. - common.js: speak(answer) dopo ogni risposta ARIA (voce discreta vol .55, markdown strip), toggle 🔊/🔇 nell'header (muto persistito localStorage nis2_aria_voice), autoplay best-effort. Smoke: voice-ms da nis2-app HTTP 200 audio/mpeg 19KB; /api/ai/tts 401 senza auth. Reachability OK (no allowlist VOX necessaria). Cache-buster common.js -> 20260624g. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fe7d0555c6
commit
a982797fa2
@@ -115,6 +115,12 @@ define('VOCEA_BASE_URL', Env::get('VOCEA_BASE_URL', 'https://api.vocea.cloud
|
|||||||
define('VOCEA_API_KEY', Env::get('VOCEA_API_KEY', '')); // wbk_... per-tenant (da vault)
|
define('VOCEA_API_KEY', Env::get('VOCEA_API_KEY', '')); // wbk_... per-tenant (da vault)
|
||||||
define('VOCEA_PORTAL_URL', Env::get('VOCEA_PORTAL_URL', 'https://app.vocea.cloud')); // portale embed via wb-link.js
|
define('VOCEA_PORTAL_URL', Env::get('VOCEA_PORTAL_URL', 'https://app.vocea.cloud')); // portale embed via wb-link.js
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// VOICE / TTS (ARIA voce naturale — proxy same-origin a nexus-voice-ms AgileHub)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
define('VOICE_MS_URL', Env::get('VOICE_MS_URL', 'http://172.21.0.1:4215/tts/speak')); // gateway nis2-network → host:4215
|
||||||
|
define('TTS_VOICE_ID', Env::get('TTS_VOICE_ID', 'EXAVITQu4vr4xnSDxMaL')); // voce "Sarah" (come AllTax/TRPG)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// CERTISOURCE (atti-service.php)
|
// CERTISOURCE (atti-service.php)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -97,4 +97,78 @@ class AiController extends BaseController
|
|||||||
$this->jsonError('Assistente AI non disponibile in questo momento', 503, 'AI_UNAVAILABLE');
|
$this->jsonError('Assistente AI non disponibile in questo momento', 503, 'AI_UNAVAILABLE');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/ai/tts — Voce naturale di ARIA.
|
||||||
|
* Proxy same-origin verso nexus-voice-ms (ElevenLabs): il voice-ms non espone CORS
|
||||||
|
* per l'origin NIS2 → il browser non può chiamarlo diretto. Cache MP3 per hash del
|
||||||
|
* testo (stesse frasi → niente costo ripetuto). Stream audio/mpeg.
|
||||||
|
*/
|
||||||
|
public function tts(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$text = trim((string) $this->getParam('text', ''));
|
||||||
|
if ($text === '') { $this->jsonError('Testo mancante', 422, 'TTS_EMPTY'); }
|
||||||
|
if (mb_strlen($text) > 800) { $text = mb_substr($text, 0, 800); }
|
||||||
|
|
||||||
|
$lang = substr(strtolower(preg_replace('/[^a-z]/', '', (string) $this->getParam('lang', 'it')) ?: 'it'), 0, 5) ?: 'it';
|
||||||
|
$voice = (string) $this->getParam('voice_id', defined('TTS_VOICE_ID') ? TTS_VOICE_ID : 'EXAVITQu4vr4xnSDxMaL');
|
||||||
|
if (!preg_match('/^[A-Za-z0-9]{8,40}$/', $voice)) {
|
||||||
|
$voice = defined('TTS_VOICE_ID') ? TTS_VOICE_ID : 'EXAVITQu4vr4xnSDxMaL';
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheDir = sys_get_temp_dir() . '/nis2-tts-cache';
|
||||||
|
$cacheFile = $cacheDir . '/' . hash('sha256', $voice . '|' . $lang . '|' . $text) . '.mp3';
|
||||||
|
if (is_file($cacheFile) && filesize($cacheFile) > 256) {
|
||||||
|
$this->streamMp3((string) file_get_contents($cacheFile), 'HIT');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_encode([
|
||||||
|
'text' => $text,
|
||||||
|
'lang' => $lang,
|
||||||
|
'voice_id' => $voice,
|
||||||
|
'model' => 'eleven_turbo_v2_5',
|
||||||
|
'stability' => 0.5,
|
||||||
|
'similarity_boost' => 0.75,
|
||||||
|
'style' => 0.0,
|
||||||
|
'output_format' => 'mp3_44100_128',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$url = defined('VOICE_MS_URL') ? VOICE_MS_URL : 'http://172.21.0.1:4215/tts/speak';
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 6,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
]);
|
||||||
|
$audio = curl_exec($ch);
|
||||||
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($code !== 200 || !$audio || strlen($audio) < 256 || stripos($ctype, 'audio') === false) {
|
||||||
|
$this->jsonError('Sintesi vocale non disponibile', 502, 'TTS_UPSTREAM');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($cacheDir)) { @mkdir($cacheDir, 0775, true); }
|
||||||
|
@file_put_contents($cacheFile . '.tmp', $audio);
|
||||||
|
@rename($cacheFile . '.tmp', $cacheFile);
|
||||||
|
|
||||||
|
$this->streamMp3($audio, 'MISS');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function streamMp3(string $audio, string $cache): void
|
||||||
|
{
|
||||||
|
header('Content-Type: audio/mpeg');
|
||||||
|
header('Content-Length: ' . strlen($audio));
|
||||||
|
header('Cache-Control: private, max-age=86400');
|
||||||
|
header('X-TTS-Cache: ' . $cache);
|
||||||
|
echo $audio;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
|
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
|
||||||
<script src="js/api.js?v=20260631"></script>
|
<script src="js/api.js?v=20260631"></script>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<script src="js/common-bi.js?v=20260624f"></script>
|
<script src="js/common-bi.js?v=20260624f"></script>
|
||||||
<script>
|
<script>
|
||||||
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
|
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260620"></script>
|
<script src="/js/api.js?v=20260620"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../js/api.js?v=20260620"></script>
|
<script src="../js/api.js?v=20260620"></script>
|
||||||
<script src="../js/common.js?v=20260624f"></script>
|
<script src="../js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -170,7 +170,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../js/api.js?v=20260620"></script>
|
<script src="../js/api.js?v=20260620"></script>
|
||||||
<script src="../js/common.js?v=20260624f"></script>
|
<script src="../js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -1091,7 +1091,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260631"></script>
|
<script src="js/api.js?v=20260631"></script>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="js/i18n.js?v=20260631"></script>
|
<script src="js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -155,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
+1
-1
@@ -362,7 +362,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260623g"></script>
|
<script src="/js/api.js?v=20260623g"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -350,7 +350,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260631"></script>
|
<script src="js/api.js?v=20260631"></script>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
|
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
|
||||||
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
|
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -383,7 +383,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
+1
-1
@@ -1143,7 +1143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
|
|||||||
@@ -352,7 +352,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -698,6 +698,7 @@ $actionMap = [
|
|||||||
|
|
||||||
'ai' => [
|
'ai' => [
|
||||||
'POST:ask' => 'ask',
|
'POST:ask' => 'ask',
|
||||||
|
'POST:tts' => 'tts', // voce naturale ARIA (proxy nexus-voice-ms)
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── BrandingController — White-label firm (Fase 5 / G16) ──
|
// ── BrandingController — White-label firm (Fase 5 / G16) ──
|
||||||
|
|||||||
@@ -912,7 +912,7 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<script src="js/i18n.js?v=20260631"></script>
|
<script src="js/i18n.js?v=20260631"></script>
|
||||||
<script>
|
<script>
|
||||||
function showTab(id) {
|
function showTab(id) {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -1014,6 +1014,42 @@ function switchLang(lang) {
|
|||||||
+ "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;";
|
+ "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;";
|
||||||
document.body.appendChild(panel);
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
// ── Voce ARIA naturale (TTS via /api/ai/tts → nexus-voice-ms) ─────────────
|
||||||
|
var ttsMuted = localStorage.getItem('nis2_aria_voice') === 'off';
|
||||||
|
var ttsAudio = null;
|
||||||
|
function _ttsPlain(s) {
|
||||||
|
return String(s || '')
|
||||||
|
.replace(/```[\s\S]*?```/g, ' ')
|
||||||
|
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||||
|
.replace(/[#*_>`|]/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ').trim().slice(0, 800);
|
||||||
|
}
|
||||||
|
function speak(text) {
|
||||||
|
if (ttsMuted) return;
|
||||||
|
var clean = _ttsPlain(text);
|
||||||
|
if (!clean) return;
|
||||||
|
var tok = localStorage.getItem('nis2_access_token') || '';
|
||||||
|
fetch('/api/ai/tts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': tok ? ('Bearer ' + tok) : '' },
|
||||||
|
body: JSON.stringify({ text: clean })
|
||||||
|
}).then(function (r) { return r.ok ? r.blob() : null; })
|
||||||
|
.then(function (b) {
|
||||||
|
if (!b || ttsMuted) return;
|
||||||
|
try { if (ttsAudio) ttsAudio.pause(); } catch (e) {}
|
||||||
|
ttsAudio = new Audio(URL.createObjectURL(b));
|
||||||
|
ttsAudio.volume = 0.55;
|
||||||
|
ttsAudio.play().catch(function () {}); // autoplay bloccato → silenzioso
|
||||||
|
}).catch(function () {});
|
||||||
|
}
|
||||||
|
function toggleVoice() {
|
||||||
|
ttsMuted = !ttsMuted;
|
||||||
|
localStorage.setItem('nis2_aria_voice', ttsMuted ? 'off' : 'on');
|
||||||
|
if (ttsMuted && ttsAudio) { try { ttsAudio.pause(); } catch (e) {} }
|
||||||
|
var vb = document.getElementById('ai-chat-voice');
|
||||||
|
if (vb) vb.textContent = ttsMuted ? '🔇' : '🔊';
|
||||||
|
}
|
||||||
|
|
||||||
var STORAGE_KEY = 'nis2_ai_chat';
|
var STORAGE_KEY = 'nis2_ai_chat';
|
||||||
var history = [];
|
var history = [];
|
||||||
var pending = false; // ARIA sta elaborando una risposta (mostra indicatore "sta scrivendo")
|
var pending = false; // ARIA sta elaborando una risposta (mostra indicatore "sta scrivendo")
|
||||||
@@ -1071,6 +1107,8 @@ function switchLang(lang) {
|
|||||||
+ '<div style="display:flex;align-items:center;gap:8px;font-weight:600">'
|
+ '<div style="display:flex;align-items:center;gap:8px;font-weight:600">'
|
||||||
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><path d="M12 3l1.8 4.6L18.5 9.5l-4.7 1.9L12 16l-1.8-4.6L5.5 9.5l4.7-1.9L12 3z"/></svg> ARIA — Assistente NIS2'
|
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px"><path d="M12 3l1.8 4.6L18.5 9.5l-4.7 1.9L12 16l-1.8-4.6L5.5 9.5l4.7-1.9L12 3z"/></svg> ARIA — Assistente NIS2'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
|
+ '<button id="ai-chat-voice" aria-label="Attiva/disattiva voce" title="Voce ARIA" style="background:none;'
|
||||||
|
+ 'border:none;color:#fff;font-size:15px;cursor:pointer;margin-right:2px">' + (ttsMuted ? '🔇' : '🔊') + '</button>'
|
||||||
+ '<button id="ai-chat-close" aria-label="Chiudi" style="background:none;'
|
+ '<button id="ai-chat-close" aria-label="Chiudi" style="background:none;'
|
||||||
+ 'border:none;color:#fff;font-size:18px;cursor:pointer">×</button>'
|
+ 'border:none;color:#fff;font-size:18px;cursor:pointer">×</button>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
@@ -1106,6 +1144,8 @@ function switchLang(lang) {
|
|||||||
+ '</button>'
|
+ '</button>'
|
||||||
+ '</form>';
|
+ '</form>';
|
||||||
document.getElementById('ai-chat-close').onclick = closePanel;
|
document.getElementById('ai-chat-close').onclick = closePanel;
|
||||||
|
var _vb = document.getElementById('ai-chat-voice');
|
||||||
|
if (_vb) _vb.onclick = toggleVoice;
|
||||||
document.getElementById('ai-chat-form').onsubmit = onSubmit;
|
document.getElementById('ai-chat-form').onsubmit = onSubmit;
|
||||||
var inputEl = document.getElementById('ai-chat-input');
|
var inputEl = document.getElementById('ai-chat-input');
|
||||||
if (inputEl) {
|
if (inputEl) {
|
||||||
@@ -1165,6 +1205,7 @@ function switchLang(lang) {
|
|||||||
|| (resp && (resp.answer || resp.message || resp.text))
|
|| (resp && (resp.answer || resp.message || resp.text))
|
||||||
|| 'ARIA non ha risposto. Riprova tra poco.';
|
|| 'ARIA non ha risposto. Riprova tra poco.';
|
||||||
history.push({ role: 'assistant', content: answer });
|
history.push({ role: 'assistant', content: answer });
|
||||||
|
try { speak(answer); } catch (e2) {} // voce naturale ARIA (se non mutata)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
history.push({
|
history.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@
|
|||||||
|
|
||||||
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
|
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
|
||||||
<script src="js/api.js?v=20260631"></script>
|
<script src="js/api.js?v=20260631"></script>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<script>
|
<script>
|
||||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -426,6 +426,6 @@ curl https://nis2.agile.software/api/services/status</pre>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -495,7 +495,7 @@
|
|||||||
|
|
||||||
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
|
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
|
||||||
<script src="js/api.js?v=20260631"></script>
|
<script src="js/api.js?v=20260631"></script>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ──────────────────────────────────────────────────
|
// ── Auth check ──────────────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -334,7 +334,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
+1
-1
@@ -132,7 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -269,7 +269,7 @@
|
|||||||
|
|
||||||
<!-- Stessa logica della register.html: ZERO modifiche backend -->
|
<!-- Stessa logica della register.html: ZERO modifiche backend -->
|
||||||
<script src="js/api.js?v=20260631"></script>
|
<script src="js/api.js?v=20260631"></script>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<script>
|
<script>
|
||||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -444,7 +444,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
+1
-1
@@ -495,7 +495,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -689,7 +689,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -195,7 +195,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -192,7 +192,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
@@ -478,7 +478,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -293,7 +293,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -208,7 +208,7 @@
|
|||||||
<!-- Report Detail Modal handled by common.js showModal -->
|
<!-- Report Detail Modal handled by common.js showModal -->
|
||||||
|
|
||||||
<script src="/js/api.js?v=20260631"></script>
|
<script src="/js/api.js?v=20260631"></script>
|
||||||
<script src="/js/common.js?v=20260624f"></script>
|
<script src="/js/common.js?v=20260624g"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||||
riusando tutti gli helper di common.js (zero backend). -->
|
riusando tutti gli helper di common.js (zero backend). -->
|
||||||
|
|||||||
@@ -322,7 +322,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api.js?v=20260631"></script>
|
<script src="js/api.js?v=20260631"></script>
|
||||||
<script src="js/common.js?v=20260624f"></script>
|
<script src="js/common.js?v=20260624g"></script>
|
||||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||||
<script src="js/i18n.js?v=20260631"></script>
|
<script src="js/i18n.js?v=20260631"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user