@lokutor/sdk 1.1.19 → 1.1.21

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