@lokutor/sdk 1.1.19 → 1.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AUDIO_CONFIG: () => AUDIO_CONFIG,
24
24
  BrowserAudioManager: () => BrowserAudioManager,
25
+ ConversationalPanel: () => ConversationalPanel,
25
26
  DEFAULT_URLS: () => DEFAULT_URLS,
26
27
  Language: () => Language,
27
28
  LokutorError: () => LokutorError,
@@ -1252,10 +1253,585 @@ async function simpleTTS(options) {
1252
1253
  const client = new TTSClient({ apiKey: options.apiKey });
1253
1254
  return client.synthesize(options);
1254
1255
  }
1256
+
1257
+ // src/conversational-panel.ts
1258
+ var PANEL_CSS = (
1259
+ /*css*/
1260
+ `
1261
+ .cv-panel {
1262
+ position: relative;
1263
+ width: 100%;
1264
+ aspect-ratio: 16 / 9;
1265
+ max-height: 800px;
1266
+ display: flex;
1267
+ flex-direction: column;
1268
+ align-items: center;
1269
+ justify-content: center;
1270
+ padding: 2rem 0;
1271
+ margin: 0;
1272
+ background: var(--cv-bg, #0a0a0a);
1273
+ box-shadow: inset 0 10px 40px rgba(0, 0, 0, 0.1), inset 0 0 100px rgba(0, 0, 0, 0.05);
1274
+ border-radius: 40px;
1275
+ overflow: hidden;
1276
+ container-type: inline-size;
1277
+ }
1278
+ .cv-curtain {
1279
+ position: absolute;
1280
+ inset: 0;
1281
+ display: flex;
1282
+ flex-direction: column;
1283
+ align-items: center;
1284
+ justify-content: center;
1285
+ background: var(--cv-bg, #0a0a0a);
1286
+ z-index: 100;
1287
+ transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1);
1288
+ }
1289
+ .cv-curtain .cv-curtain-bg {
1290
+ position: absolute;
1291
+ inset: 0;
1292
+ background: url('/background_gradient.jpeg') center / cover no-repeat;
1293
+ z-index: -1;
1294
+ }
1295
+ .cv-curtain .cv-curtain-overlay {
1296
+ position: absolute;
1297
+ inset: 0;
1298
+ background: var(--cv-accent);
1299
+ opacity: 0.35;
1300
+ z-index: -1;
1301
+ }
1302
+ .cv-curtain.is-up { transform: translateY(-100%); }
1303
+ .cv-curtain-content {
1304
+ display: flex;
1305
+ flex-direction: column;
1306
+ align-items: center;
1307
+ gap: clamp(0.75rem, 1.5cqi, 1.5rem);
1308
+ color: #fff;
1309
+ text-align: center;
1310
+ z-index: 2;
1311
+ padding: clamp(1rem, 2cqi, 2rem);
1312
+ }
1313
+ .cv-curtain-title {
1314
+ font-size: clamp(1.2rem, 4cqi, 2.5rem);
1315
+ font-weight: 800;
1316
+ letter-spacing: -0.03em;
1317
+ margin: 0;
1318
+ text-shadow: 0 4px 20px rgba(0,0,0,0.3);
1319
+ }
1320
+ .cv-curtain-desc {
1321
+ font-size: clamp(0.75rem, 2cqi, 1.1rem);
1322
+ opacity: 0.8;
1323
+ max-width: 400px;
1324
+ margin: 0;
1325
+ line-height: 1.5;
1326
+ }
1327
+ .cv-curtain-btn {
1328
+ margin-top: 1rem;
1329
+ padding: clamp(0.6rem, 1.5cqi, 1rem) clamp(1.5rem, 3cqi, 2.5rem);
1330
+ border-radius: 100px;
1331
+ background: #fff;
1332
+ color: #000;
1333
+ border: none;
1334
+ font-weight: 700;
1335
+ font-size: clamp(0.8rem, 1.5cqi, 1rem);
1336
+ display: flex;
1337
+ align-items: center;
1338
+ gap: 0.75rem;
1339
+ cursor: pointer;
1340
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
1341
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
1342
+ }
1343
+ .cv-curtain-btn:hover {
1344
+ transform: scale(1.05);
1345
+ background: #f0f0f0;
1346
+ box-shadow: 0 15px 40px rgba(0,0,0,0.3);
1347
+ }
1348
+ .cv-curtain-btn svg { transition: transform 0.3s ease; }
1349
+ .cv-curtain-btn:hover svg { transform: translateX(4px); }
1350
+ .cv-header {
1351
+ width: 100%;
1352
+ display: flex;
1353
+ flex-direction: column;
1354
+ align-items: center;
1355
+ gap: 0.25rem;
1356
+ z-index: 20;
1357
+ margin-bottom: auto;
1358
+ }
1359
+ .cv-title {
1360
+ font-size: clamp(1rem, 2.5cqi, 1.75rem);
1361
+ font-weight: 700;
1362
+ color: #e0e0e0;
1363
+ display: flex;
1364
+ align-items: center;
1365
+ gap: 1rem;
1366
+ letter-spacing: -0.02em;
1367
+ }
1368
+ .cv-title .cv-timer {
1369
+ font-variant-numeric: tabular-nums;
1370
+ color: var(--cv-accent);
1371
+ font-weight: 400;
1372
+ opacity: 0.8;
1373
+ }
1374
+ .cv-visualizer-wrap {
1375
+ position: absolute;
1376
+ top: 50%;
1377
+ left: 50%;
1378
+ transform: translate(-50%, -50%);
1379
+ width: clamp(140px, 40cqi, 280px);
1380
+ height: clamp(140px, 40cqi, 280px);
1381
+ display: flex;
1382
+ align-items: center;
1383
+ justify-content: center;
1384
+ z-index: 10;
1385
+ transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
1386
+ }
1387
+ .cv-is-speaking .cv-visualizer-wrap {
1388
+ animation: cv-pulse 2.5s infinite ease-in-out;
1389
+ }
1390
+ .cv-is-thinking .cv-visualizer-wrap {
1391
+ opacity: 0.5;
1392
+ transform: translate(-50%, -50%) scale(0.9);
1393
+ }
1394
+ @keyframes cv-pulse {
1395
+ 0%, 100% { transform: translate(-50%, -50%) scale(1); }
1396
+ 50% { transform: translate(-50%, -50%) scale(1.05); }
1397
+ }
1398
+ .cv-canvas {
1399
+ width: 100% !important;
1400
+ height: 100% !important;
1401
+ position: relative;
1402
+ z-index: 0;
1403
+ }
1404
+ .cv-canvas {
1405
+ width: 100% !important;
1406
+ height: 100% !important;
1407
+ position: relative;
1408
+ z-index: 0;
1409
+ }
1410
+ .cv-controls {
1411
+ width: 100%;
1412
+ display: flex;
1413
+ flex-direction: column;
1414
+ align-items: center;
1415
+ gap: 1.5rem;
1416
+ padding-top: 1rem;
1417
+ margin-top: auto;
1418
+ z-index: 20;
1419
+ }
1420
+ .cv-pill {
1421
+ display: flex;
1422
+ align-items: center;
1423
+ gap: 0.75rem;
1424
+ background: rgba(255, 255, 255, 0.03);
1425
+ backdrop-filter: blur(30px);
1426
+ -webkit-backdrop-filter: blur(30px);
1427
+ border: 1px solid rgba(255, 255, 255, 0.08);
1428
+ padding: 0.3rem 0.75rem;
1429
+ border-radius: 100px;
1430
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
1431
+ }
1432
+ .cv-btn {
1433
+ display: flex;
1434
+ align-items: center;
1435
+ gap: 0.5rem;
1436
+ background: transparent;
1437
+ border: none;
1438
+ color: rgba(255,255,255,0.55);
1439
+ font-weight: 600;
1440
+ font-size: 0.75rem;
1441
+ cursor: pointer;
1442
+ transition: all 0.2s ease;
1443
+ padding: 0.35rem 0.75rem;
1444
+ border-radius: 50px;
1445
+ }
1446
+ .cv-btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
1447
+ .cv-btn--end { color: #ff4444; }
1448
+ .cv-btn--end .cv-btn-box {
1449
+ background: #ff4444;
1450
+ width: 8px;
1451
+ height: 8px;
1452
+ border-radius: 2px;
1453
+ }
1454
+ .cv-btn.is-muted { color: var(--cv-accent); }
1455
+ .cv-error {
1456
+ position: absolute;
1457
+ bottom: 1.25rem;
1458
+ left: 50%;
1459
+ transform: translateX(-50%);
1460
+ background: #0a0a0a;
1461
+ color: #e0e0e0;
1462
+ padding: 0.75rem 1.5rem;
1463
+ border-radius: 12px;
1464
+ font-size: 0.875rem;
1465
+ font-weight: 500;
1466
+ display: none;
1467
+ align-items: center;
1468
+ gap: 0.75rem;
1469
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
1470
+ z-index: 1000;
1471
+ border: 1px solid rgba(255, 255, 255, 0.06);
1472
+ backdrop-filter: blur(20px);
1473
+ }
1474
+ .cv-error.is-visible { display: flex; }
1475
+ .cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
1476
+ `
1477
+ );
1478
+ var styleInjected = false;
1479
+ function injectStyles() {
1480
+ if (styleInjected) return;
1481
+ const style = document.createElement("style");
1482
+ style.textContent = PANEL_CSS;
1483
+ document.head.appendChild(style);
1484
+ styleInjected = true;
1485
+ }
1486
+ var ConversationalPanel = class {
1487
+ cfg;
1488
+ container;
1489
+ agent = null;
1490
+ visualizer = null;
1491
+ timerTicker = null;
1492
+ connectingSound = null;
1493
+ connectingFadeTimer = null;
1494
+ isRunning = false;
1495
+ // Cached DOM refs
1496
+ el;
1497
+ curtain;
1498
+ curtainTitle;
1499
+ curtainDesc;
1500
+ startBtn;
1501
+ errorEl;
1502
+ errorText;
1503
+ timerEl;
1504
+ canvas;
1505
+ muteBtn;
1506
+ stopBtn;
1507
+ visualizerWrap;
1508
+ // Callbacks
1509
+ onTranscription;
1510
+ onResponse;
1511
+ onStart;
1512
+ onStop;
1513
+ onError;
1514
+ constructor(cfg) {
1515
+ this.cfg = cfg;
1516
+ this.container = cfg.container;
1517
+ injectStyles();
1518
+ this.buildDOM();
1519
+ }
1520
+ buildDOM() {
1521
+ const accent = this.cfg.accentColor || "#a25a6b";
1522
+ const bg = this.cfg.backgroundColor || "#0a0a0a";
1523
+ this.container.style.setProperty("--cv-accent", accent);
1524
+ this.container.style.setProperty("--cv-bg", bg);
1525
+ this.el = document.createElement("div");
1526
+ this.el.className = "cv-panel";
1527
+ this.el.innerHTML = `
1528
+ <div class="cv-curtain">
1529
+ <div class="cv-curtain-bg"></div>
1530
+ <div class="cv-curtain-overlay"></div>
1531
+ <div class="cv-curtain-content">
1532
+ <h3 class="cv-curtain-title">${this.esc(this.cfg.title)}</h3>
1533
+ <p class="cv-curtain-desc">${this.esc(this.cfg.description)}</p>
1534
+ <button class="cv-curtain-btn">
1535
+ <span>Start Conversation</span>
1536
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20">
1537
+ <path d="M5 12h14M12 5l7 7-7 7"/>
1538
+ </svg>
1539
+ </button>
1540
+ </div>
1541
+ <div class="cv-error">
1542
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1543
+ width="20" height="20" class="cv-error-icon">
1544
+ <circle cx="12" cy="12" r="10"></circle>
1545
+ <path d="M12 8v4"></path>
1546
+ <path d="M12 16h.01"></path>
1547
+ </svg>
1548
+ <span class="cv-error-text"></span>
1549
+ </div>
1550
+ </div>
1551
+ <div class="cv-header">
1552
+ <h2 class="cv-title">${this.esc(this.cfg.title)} <span class="cv-timer">00:00</span></h2>
1553
+ </div>
1554
+ <div class="cv-visualizer-wrap">
1555
+ <canvas class="cv-canvas"></canvas>
1556
+ </div>
1557
+ <div class="cv-controls">
1558
+ <div class="cv-pill">
1559
+ <button class="cv-btn cv-btn--mute">
1560
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="18" height="18">
1561
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
1562
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
1563
+ </svg>
1564
+ <span>Mute</span>
1565
+ </button>
1566
+ <button class="cv-btn cv-btn--end">
1567
+ <div class="cv-btn-box"></div>
1568
+ <span>End call</span>
1569
+ </button>
1570
+ </div>
1571
+ </div>
1572
+ `;
1573
+ this.container.appendChild(this.el);
1574
+ this.curtain = this.el.querySelector(".cv-curtain");
1575
+ if (this.cfg.curtainBgSrc) {
1576
+ this.curtain.style.backgroundImage = `url('${this.cfg.curtainBgSrc}')`;
1577
+ }
1578
+ this.curtainTitle = this.el.querySelector(".cv-curtain-title");
1579
+ this.curtainDesc = this.el.querySelector(".cv-curtain-desc");
1580
+ this.startBtn = this.el.querySelector(".cv-curtain-btn");
1581
+ this.errorEl = this.el.querySelector(".cv-error");
1582
+ this.errorText = this.el.querySelector(".cv-error-text");
1583
+ this.timerEl = this.el.querySelector(".cv-timer");
1584
+ this.canvas = this.el.querySelector(".cv-canvas");
1585
+ this.muteBtn = this.el.querySelector(".cv-btn--mute");
1586
+ this.stopBtn = this.el.querySelector(".cv-btn--end");
1587
+ this.visualizerWrap = this.el.querySelector(".cv-visualizer-wrap");
1588
+ this.startBtn.addEventListener("click", () => this.start());
1589
+ this.stopBtn.addEventListener("click", () => this.stop());
1590
+ this.muteBtn.addEventListener("click", () => this.toggleMute());
1591
+ }
1592
+ esc(s) {
1593
+ const d = document.createElement("div");
1594
+ d.textContent = s;
1595
+ return d.innerHTML;
1596
+ }
1597
+ /** Start the conversation (called when user clicks "Start Conversation" or externally) */
1598
+ async start() {
1599
+ if (this.isRunning) return;
1600
+ this.isRunning = true;
1601
+ this.startBtn.disabled = true;
1602
+ this.errorEl.classList.remove("is-visible");
1603
+ this.playConnectingSound();
1604
+ try {
1605
+ const ConvoAgent = this.cfg.ConvoAgent;
1606
+ const SphereVisualizer = this.cfg.SphereVisualizer;
1607
+ const visualizer = new SphereVisualizer(this.canvas);
1608
+ visualizer.resize(280, 280);
1609
+ this.visualizer = visualizer;
1610
+ if (this.cfg.accentColor && typeof visualizer.setAccentColor === "function") {
1611
+ const hex = this.cfg.accentColor;
1612
+ visualizer.setAccentColor(
1613
+ parseInt(hex.slice(1, 3), 16) / 255,
1614
+ parseInt(hex.slice(3, 5), 16) / 255,
1615
+ parseInt(hex.slice(5, 7), 16) / 255
1616
+ );
1617
+ }
1618
+ const agentHandle = new ConvoAgent({
1619
+ apiKey: this.cfg.apiKey,
1620
+ prompt: this.cfg.prompt,
1621
+ voice: this.cfg.voice || "M1",
1622
+ language: this.cfg.language || "en",
1623
+ onStatusChange: (status) => {
1624
+ this.el.classList.remove("cv-is-speaking", "cv-is-thinking");
1625
+ if (status === "speaking") this.el.classList.add("cv-is-speaking");
1626
+ else if (status === "thinking") this.el.classList.add("cv-is-thinking");
1627
+ },
1628
+ onTranscription: (text) => {
1629
+ this.onTranscription?.(text);
1630
+ },
1631
+ onResponse: (text) => {
1632
+ this.onResponse?.(text);
1633
+ },
1634
+ onError: (err) => {
1635
+ if (!this.isRunning) return;
1636
+ this.onError?.(err);
1637
+ this.stop();
1638
+ this.showError("Connection issue. Try again in a moment.");
1639
+ }
1640
+ });
1641
+ this.agent = agentHandle;
1642
+ visualizer.setAudioClient(agentHandle);
1643
+ this.curtain.classList.add("is-up");
1644
+ try {
1645
+ agentHandle.unlockAudioForMobile?.();
1646
+ } catch (_) {
1647
+ }
1648
+ const ok = await agentHandle.connect();
1649
+ if (ok) {
1650
+ this.fadeOutConnectingSound();
1651
+ const wrap = this.visualizerWrap;
1652
+ if (wrap) {
1653
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
1654
+ const w = Math.floor(wrap.clientWidth * dpr);
1655
+ const h = Math.floor(wrap.clientHeight * dpr);
1656
+ visualizer.resize(w, h);
1657
+ }
1658
+ visualizer.start();
1659
+ this.startTimer();
1660
+ this.onStart?.();
1661
+ if (window.lucide) window.lucide.createIcons();
1662
+ } else {
1663
+ this.fadeOutConnectingSound();
1664
+ this.playErrorTone();
1665
+ this.curtain.classList.remove("is-up");
1666
+ this.showError("Could not connect. Please try again.");
1667
+ this.isRunning = false;
1668
+ }
1669
+ } catch (err) {
1670
+ this.fadeOutConnectingSound();
1671
+ this.playErrorTone();
1672
+ console.error("ConversationalPanel start error:", err);
1673
+ this.stop();
1674
+ this.showError("Something went wrong. Please try again.");
1675
+ } finally {
1676
+ this.startBtn.disabled = false;
1677
+ }
1678
+ }
1679
+ /** Stop / disconnect the conversation */
1680
+ stop() {
1681
+ if (!this.isRunning && !this.agent && !this.visualizer) return;
1682
+ this.isRunning = false;
1683
+ this.fadeOutConnectingSound();
1684
+ this.errorEl.classList.remove("is-visible");
1685
+ if (this.agent) {
1686
+ try {
1687
+ this.agent.disconnect();
1688
+ } catch (_) {
1689
+ }
1690
+ this.agent = null;
1691
+ }
1692
+ if (this.timerTicker) {
1693
+ clearInterval(this.timerTicker);
1694
+ this.timerTicker = null;
1695
+ }
1696
+ if (this.visualizer) {
1697
+ this.visualizer.stop();
1698
+ this.visualizer = null;
1699
+ }
1700
+ this.curtain.classList.remove("is-up");
1701
+ this.el.classList.remove("cv-is-speaking", "cv-is-thinking");
1702
+ this.muteBtn.classList.remove("is-muted");
1703
+ const span = this.muteBtn.querySelector("span");
1704
+ if (span) span.textContent = "Mute";
1705
+ this.onStop?.();
1706
+ this.startBtn.disabled = false;
1707
+ }
1708
+ /** Update accent color dynamically */
1709
+ setColor(color) {
1710
+ this.cfg.accentColor = color;
1711
+ this.container.style.setProperty("--cv-accent", color);
1712
+ if (this.visualizer && typeof this.visualizer.setAccentColor === "function") {
1713
+ const r = parseInt(color.slice(1, 3), 16) / 255;
1714
+ const g = parseInt(color.slice(3, 5), 16) / 255;
1715
+ const b = parseInt(color.slice(5, 7), 16) / 255;
1716
+ this.visualizer.setAccentColor(r, g, b);
1717
+ }
1718
+ }
1719
+ /** Update title text */
1720
+ setTitle(title) {
1721
+ this.cfg.title = title;
1722
+ this.curtainTitle.textContent = title;
1723
+ const h2 = this.el.querySelector(".cv-title");
1724
+ if (h2) h2.innerHTML = `${this.esc(title)} <span class="cv-timer">${this.timerEl?.textContent || "00:00"}</span>`;
1725
+ }
1726
+ /** Update description text */
1727
+ setDescription(desc) {
1728
+ this.cfg.description = desc;
1729
+ this.curtainDesc.textContent = desc;
1730
+ }
1731
+ /** Update prompt (only takes effect on next start()) */
1732
+ setPrompt(prompt) {
1733
+ this.cfg.prompt = prompt;
1734
+ }
1735
+ /** Show an error message */
1736
+ showError(text) {
1737
+ this.errorText.textContent = text;
1738
+ this.errorEl.classList.add("is-visible");
1739
+ setTimeout(() => {
1740
+ this.errorEl.classList.remove("is-visible");
1741
+ }, 5e3);
1742
+ }
1743
+ /** Destroy the component, removing all DOM and stopping any active session */
1744
+ destroy() {
1745
+ this.stop();
1746
+ this.el.remove();
1747
+ }
1748
+ // ─── internal helpers ────────────────────────────────────
1749
+ startTimer() {
1750
+ let elapsed = 0;
1751
+ this.timerEl.textContent = "00:00";
1752
+ this.timerTicker = window.setInterval(() => {
1753
+ elapsed++;
1754
+ const m = Math.floor(elapsed / 60).toString().padStart(2, "0");
1755
+ const s = (elapsed % 60).toString().padStart(2, "0");
1756
+ this.timerEl.textContent = `${m}:${s}`;
1757
+ }, 1e3);
1758
+ }
1759
+ toggleMute() {
1760
+ if (!this.agent) return;
1761
+ try {
1762
+ const muted = this.agent.toggleMute();
1763
+ this.muteBtn.classList.toggle("is-muted", muted);
1764
+ const span = this.muteBtn.querySelector("span");
1765
+ if (span) span.textContent = muted ? "Unmute" : "Mute";
1766
+ if (muted) {
1767
+ this.agent.pauseAudio?.();
1768
+ this.visualizer?.setDeactivated?.(true);
1769
+ } else {
1770
+ this.agent.resumeAudio?.();
1771
+ this.visualizer?.setDeactivated?.(false);
1772
+ }
1773
+ } catch (_) {
1774
+ }
1775
+ }
1776
+ playConnectingSound() {
1777
+ this.fadeOutConnectingSound();
1778
+ const src = this.cfg.connectingSoundSrc || "/connecting.mp3";
1779
+ this.connectingSound = new Audio(src);
1780
+ this.connectingSound.volume = 0.6;
1781
+ this.connectingSound.play().catch(() => {
1782
+ });
1783
+ }
1784
+ fadeOutConnectingSound() {
1785
+ if (this.connectingFadeTimer) {
1786
+ clearInterval(this.connectingFadeTimer);
1787
+ this.connectingFadeTimer = null;
1788
+ }
1789
+ if (!this.connectingSound) return;
1790
+ const startVol = this.connectingSound.volume;
1791
+ const steps = 15;
1792
+ let step = 0;
1793
+ this.connectingFadeTimer = window.setInterval(() => {
1794
+ step++;
1795
+ const progress = step / steps;
1796
+ if (this.connectingSound) this.connectingSound.volume = startVol * Math.max(0, 1 - progress);
1797
+ if (step >= steps) {
1798
+ if (this.connectingFadeTimer) {
1799
+ clearInterval(this.connectingFadeTimer);
1800
+ this.connectingFadeTimer = null;
1801
+ }
1802
+ if (this.connectingSound) {
1803
+ this.connectingSound.pause();
1804
+ this.connectingSound.currentTime = 0;
1805
+ this.connectingSound = null;
1806
+ }
1807
+ }
1808
+ }, 100);
1809
+ }
1810
+ playErrorTone() {
1811
+ try {
1812
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
1813
+ const now = ctx.currentTime;
1814
+ [0, 0.3].forEach((offset, i) => {
1815
+ const osc = ctx.createOscillator();
1816
+ const gain = ctx.createGain();
1817
+ osc.connect(gain);
1818
+ gain.connect(ctx.destination);
1819
+ osc.type = "sine";
1820
+ osc.frequency.setValueAtTime(600 - i * 200, now + offset);
1821
+ gain.gain.setValueAtTime(0.25, now + offset);
1822
+ gain.gain.exponentialRampToValueAtTime(1e-3, now + offset + 0.35);
1823
+ osc.start(now + offset);
1824
+ osc.stop(now + offset + 0.4);
1825
+ });
1826
+ } catch (_) {
1827
+ }
1828
+ }
1829
+ };
1255
1830
  // Annotate the CommonJS export names for ESM import in node:
1256
1831
  0 && (module.exports = {
1257
1832
  AUDIO_CONFIG,
1258
1833
  BrowserAudioManager,
1834
+ ConversationalPanel,
1259
1835
  DEFAULT_URLS,
1260
1836
  Language,
1261
1837
  LokutorError,