@lokutor/sdk 1.1.22 → 1.1.24

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.d.mts CHANGED
@@ -613,6 +613,10 @@ interface ConversationalPanelConfig {
613
613
  curtainBgSrc?: string;
614
614
  /** Tool definitions for LLM function calling */
615
615
  tools?: any[];
616
+ /** Maximum conversation duration in seconds (default 300) */
617
+ maxDuration?: number;
618
+ /** Seconds of silence before auto-close (default 30) */
619
+ silenceTimeout?: number;
616
620
  }
617
621
  declare class ConversationalPanel {
618
622
  private cfg;
@@ -623,6 +627,8 @@ declare class ConversationalPanel {
623
627
  private connectingSound;
624
628
  private connectingFadeTimer;
625
629
  private isRunning;
630
+ private _locked;
631
+ private _lastSpeechTime;
626
632
  private el;
627
633
  private curtain;
628
634
  private curtainTitle;
@@ -633,6 +639,7 @@ declare class ConversationalPanel {
633
639
  private timerEl;
634
640
  private canvas;
635
641
  private muteBtn;
642
+ private muteSvg;
636
643
  private stopBtn;
637
644
  private visualizerWrap;
638
645
  onTranscription?: (text: string) => void;
package/dist/index.d.ts CHANGED
@@ -613,6 +613,10 @@ interface ConversationalPanelConfig {
613
613
  curtainBgSrc?: string;
614
614
  /** Tool definitions for LLM function calling */
615
615
  tools?: any[];
616
+ /** Maximum conversation duration in seconds (default 300) */
617
+ maxDuration?: number;
618
+ /** Seconds of silence before auto-close (default 30) */
619
+ silenceTimeout?: number;
616
620
  }
617
621
  declare class ConversationalPanel {
618
622
  private cfg;
@@ -623,6 +627,8 @@ declare class ConversationalPanel {
623
627
  private connectingSound;
624
628
  private connectingFadeTimer;
625
629
  private isRunning;
630
+ private _locked;
631
+ private _lastSpeechTime;
626
632
  private el;
627
633
  private curtain;
628
634
  private curtainTitle;
@@ -633,6 +639,7 @@ declare class ConversationalPanel {
633
639
  private timerEl;
634
640
  private canvas;
635
641
  private muteBtn;
642
+ private muteSvg;
636
643
  private stopBtn;
637
644
  private visualizerWrap;
638
645
  onTranscription?: (text: string) => void;
package/dist/index.js CHANGED
@@ -1474,6 +1474,13 @@ var PANEL_CSS = (
1474
1474
  }
1475
1475
  .cv-error.is-visible { display: flex; }
1476
1476
  .cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
1477
+ @media (max-width: 640px) {
1478
+ .cv-panel { aspect-ratio: 3 / 4; max-height: 600px; border-radius: 20px; padding: 1rem 0; }
1479
+ .cv-curtain-content { padding: 0.75rem; gap: 0.5rem; }
1480
+ .cv-curtain-title { font-size: clamp(1rem, 5cqi, 1.6rem); }
1481
+ .cv-curtain-btn { padding: 0.5rem 1.25rem; font-size: clamp(0.7rem, 2cqi, 0.85rem); }
1482
+ .cv-visualizer-wrap { width: clamp(100px, 50cqi, 200px); height: clamp(100px, 50cqi, 200px); }
1483
+ }
1477
1484
  `
1478
1485
  );
1479
1486
  var styleInjected = false;
@@ -1493,6 +1500,8 @@ var ConversationalPanel = class {
1493
1500
  connectingSound = null;
1494
1501
  connectingFadeTimer = null;
1495
1502
  isRunning = false;
1503
+ _locked = false;
1504
+ _lastSpeechTime = 0;
1496
1505
  // Cached DOM refs
1497
1506
  el;
1498
1507
  curtain;
@@ -1504,6 +1513,7 @@ var ConversationalPanel = class {
1504
1513
  timerEl;
1505
1514
  canvas;
1506
1515
  muteBtn;
1516
+ muteSvg;
1507
1517
  stopBtn;
1508
1518
  visualizerWrap;
1509
1519
  // Callbacks
@@ -1585,6 +1595,7 @@ var ConversationalPanel = class {
1585
1595
  this.timerEl = this.el.querySelector(".cv-timer");
1586
1596
  this.canvas = this.el.querySelector(".cv-canvas");
1587
1597
  this.muteBtn = this.el.querySelector(".cv-btn--mute");
1598
+ this.muteSvg = this.muteBtn.querySelector("svg");
1588
1599
  this.stopBtn = this.el.querySelector(".cv-btn--end");
1589
1600
  this.visualizerWrap = this.el.querySelector(".cv-visualizer-wrap");
1590
1601
  this.startBtn.addEventListener("click", () => this.start());
@@ -1599,6 +1610,10 @@ var ConversationalPanel = class {
1599
1610
  /** Start the conversation (called when user clicks "Start Conversation" or externally) */
1600
1611
  async start() {
1601
1612
  if (this.isRunning) return;
1613
+ if (this._locked) {
1614
+ this.showError("This session has ended. Refresh the page to start a new one.");
1615
+ return;
1616
+ }
1602
1617
  this.isRunning = true;
1603
1618
  this.startBtn.disabled = true;
1604
1619
  this.errorEl.classList.remove("is-visible");
@@ -1632,6 +1647,7 @@ var ConversationalPanel = class {
1632
1647
  this.onTranscription?.(text);
1633
1648
  },
1634
1649
  onResponse: (text) => {
1650
+ this._lastSpeechTime = Date.now();
1635
1651
  this.onResponse?.(text);
1636
1652
  },
1637
1653
  onToolCall: (tool) => {
@@ -1758,12 +1774,26 @@ var ConversationalPanel = class {
1758
1774
  // ─── internal helpers ────────────────────────────────────
1759
1775
  startTimer() {
1760
1776
  let elapsed = 0;
1777
+ const maxDur = this.cfg.maxDuration || 300;
1778
+ const silentMax = this.cfg.silenceTimeout || 30;
1779
+ this._lastSpeechTime = Date.now();
1761
1780
  this.timerEl.textContent = "00:00";
1762
1781
  this.timerTicker = window.setInterval(() => {
1763
1782
  elapsed++;
1764
1783
  const m = Math.floor(elapsed / 60).toString().padStart(2, "0");
1765
1784
  const s = (elapsed % 60).toString().padStart(2, "0");
1766
1785
  this.timerEl.textContent = `${m}:${s}`;
1786
+ if (elapsed >= maxDur) {
1787
+ this._locked = true;
1788
+ this.stop();
1789
+ this.showError("Conversation time limit reached. Refresh to start a new one.");
1790
+ return;
1791
+ }
1792
+ if (Date.now() - this._lastSpeechTime > silentMax * 1e3) {
1793
+ this._locked = true;
1794
+ this.stop();
1795
+ this.showError("Session ended due to inactivity. Refresh to start a new one.");
1796
+ }
1767
1797
  }, 1e3);
1768
1798
  }
1769
1799
  toggleMute() {
@@ -1773,12 +1803,8 @@ var ConversationalPanel = class {
1773
1803
  this.muteBtn.classList.toggle("is-muted", muted);
1774
1804
  const span = this.muteBtn.querySelector("span");
1775
1805
  if (span) span.textContent = muted ? "Unmute" : "Mute";
1776
- if (muted) {
1777
- this.agent.pauseAudio?.();
1778
- this.visualizer?.setDeactivated?.(true);
1779
- } else {
1780
- this.agent.resumeAudio?.();
1781
- this.visualizer?.setDeactivated?.(false);
1806
+ if (this.muteSvg) {
1807
+ this.muteSvg.innerHTML = muted ? '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="23" y1="1" x2="1" y2="23"/>' : '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/>';
1782
1808
  }
1783
1809
  } catch (_) {
1784
1810
  }
package/dist/index.mjs CHANGED
@@ -1427,6 +1427,13 @@ var PANEL_CSS = (
1427
1427
  }
1428
1428
  .cv-error.is-visible { display: flex; }
1429
1429
  .cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
1430
+ @media (max-width: 640px) {
1431
+ .cv-panel { aspect-ratio: 3 / 4; max-height: 600px; border-radius: 20px; padding: 1rem 0; }
1432
+ .cv-curtain-content { padding: 0.75rem; gap: 0.5rem; }
1433
+ .cv-curtain-title { font-size: clamp(1rem, 5cqi, 1.6rem); }
1434
+ .cv-curtain-btn { padding: 0.5rem 1.25rem; font-size: clamp(0.7rem, 2cqi, 0.85rem); }
1435
+ .cv-visualizer-wrap { width: clamp(100px, 50cqi, 200px); height: clamp(100px, 50cqi, 200px); }
1436
+ }
1430
1437
  `
1431
1438
  );
1432
1439
  var styleInjected = false;
@@ -1446,6 +1453,8 @@ var ConversationalPanel = class {
1446
1453
  connectingSound = null;
1447
1454
  connectingFadeTimer = null;
1448
1455
  isRunning = false;
1456
+ _locked = false;
1457
+ _lastSpeechTime = 0;
1449
1458
  // Cached DOM refs
1450
1459
  el;
1451
1460
  curtain;
@@ -1457,6 +1466,7 @@ var ConversationalPanel = class {
1457
1466
  timerEl;
1458
1467
  canvas;
1459
1468
  muteBtn;
1469
+ muteSvg;
1460
1470
  stopBtn;
1461
1471
  visualizerWrap;
1462
1472
  // Callbacks
@@ -1538,6 +1548,7 @@ var ConversationalPanel = class {
1538
1548
  this.timerEl = this.el.querySelector(".cv-timer");
1539
1549
  this.canvas = this.el.querySelector(".cv-canvas");
1540
1550
  this.muteBtn = this.el.querySelector(".cv-btn--mute");
1551
+ this.muteSvg = this.muteBtn.querySelector("svg");
1541
1552
  this.stopBtn = this.el.querySelector(".cv-btn--end");
1542
1553
  this.visualizerWrap = this.el.querySelector(".cv-visualizer-wrap");
1543
1554
  this.startBtn.addEventListener("click", () => this.start());
@@ -1552,6 +1563,10 @@ var ConversationalPanel = class {
1552
1563
  /** Start the conversation (called when user clicks "Start Conversation" or externally) */
1553
1564
  async start() {
1554
1565
  if (this.isRunning) return;
1566
+ if (this._locked) {
1567
+ this.showError("This session has ended. Refresh the page to start a new one.");
1568
+ return;
1569
+ }
1555
1570
  this.isRunning = true;
1556
1571
  this.startBtn.disabled = true;
1557
1572
  this.errorEl.classList.remove("is-visible");
@@ -1585,6 +1600,7 @@ var ConversationalPanel = class {
1585
1600
  this.onTranscription?.(text);
1586
1601
  },
1587
1602
  onResponse: (text) => {
1603
+ this._lastSpeechTime = Date.now();
1588
1604
  this.onResponse?.(text);
1589
1605
  },
1590
1606
  onToolCall: (tool) => {
@@ -1711,12 +1727,26 @@ var ConversationalPanel = class {
1711
1727
  // ─── internal helpers ────────────────────────────────────
1712
1728
  startTimer() {
1713
1729
  let elapsed = 0;
1730
+ const maxDur = this.cfg.maxDuration || 300;
1731
+ const silentMax = this.cfg.silenceTimeout || 30;
1732
+ this._lastSpeechTime = Date.now();
1714
1733
  this.timerEl.textContent = "00:00";
1715
1734
  this.timerTicker = window.setInterval(() => {
1716
1735
  elapsed++;
1717
1736
  const m = Math.floor(elapsed / 60).toString().padStart(2, "0");
1718
1737
  const s = (elapsed % 60).toString().padStart(2, "0");
1719
1738
  this.timerEl.textContent = `${m}:${s}`;
1739
+ if (elapsed >= maxDur) {
1740
+ this._locked = true;
1741
+ this.stop();
1742
+ this.showError("Conversation time limit reached. Refresh to start a new one.");
1743
+ return;
1744
+ }
1745
+ if (Date.now() - this._lastSpeechTime > silentMax * 1e3) {
1746
+ this._locked = true;
1747
+ this.stop();
1748
+ this.showError("Session ended due to inactivity. Refresh to start a new one.");
1749
+ }
1720
1750
  }, 1e3);
1721
1751
  }
1722
1752
  toggleMute() {
@@ -1726,12 +1756,8 @@ var ConversationalPanel = class {
1726
1756
  this.muteBtn.classList.toggle("is-muted", muted);
1727
1757
  const span = this.muteBtn.querySelector("span");
1728
1758
  if (span) span.textContent = muted ? "Unmute" : "Mute";
1729
- if (muted) {
1730
- this.agent.pauseAudio?.();
1731
- this.visualizer?.setDeactivated?.(true);
1732
- } else {
1733
- this.agent.resumeAudio?.();
1734
- this.visualizer?.setDeactivated?.(false);
1759
+ if (this.muteSvg) {
1760
+ this.muteSvg.innerHTML = muted ? '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="23" y1="1" x2="1" y2="23"/>' : '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/>';
1735
1761
  }
1736
1762
  } catch (_) {
1737
1763
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokutor/sdk",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "description": "JavaScript/TypeScript SDK for Lokutor Real-time Voice AI",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -214,6 +214,13 @@ const PANEL_CSS = /*css*/ `
214
214
  }
215
215
  .cv-error.is-visible { display: flex; }
216
216
  .cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
217
+ @media (max-width: 640px) {
218
+ .cv-panel { aspect-ratio: 3 / 4; max-height: 600px; border-radius: 20px; padding: 1rem 0; }
219
+ .cv-curtain-content { padding: 0.75rem; gap: 0.5rem; }
220
+ .cv-curtain-title { font-size: clamp(1rem, 5cqi, 1.6rem); }
221
+ .cv-curtain-btn { padding: 0.5rem 1.25rem; font-size: clamp(0.7rem, 2cqi, 0.85rem); }
222
+ .cv-visualizer-wrap { width: clamp(100px, 50cqi, 200px); height: clamp(100px, 50cqi, 200px); }
223
+ }
217
224
  `;
218
225
 
219
226
  let styleInjected = false;
@@ -249,6 +256,10 @@ export interface ConversationalPanelConfig {
249
256
  curtainBgSrc?: string;
250
257
  /** Tool definitions for LLM function calling */
251
258
  tools?: any[];
259
+ /** Maximum conversation duration in seconds (default 300) */
260
+ maxDuration?: number;
261
+ /** Seconds of silence before auto-close (default 30) */
262
+ silenceTimeout?: number;
252
263
  }
253
264
 
254
265
  export class ConversationalPanel {
@@ -260,6 +271,8 @@ export class ConversationalPanel {
260
271
  private connectingSound: HTMLAudioElement | null = null;
261
272
  private connectingFadeTimer: number | null = null;
262
273
  private isRunning = false;
274
+ private _locked = false;
275
+ private _lastSpeechTime = 0;
263
276
 
264
277
  // Cached DOM refs
265
278
  private el!: HTMLElement;
@@ -272,6 +285,7 @@ export class ConversationalPanel {
272
285
  private timerEl!: HTMLElement;
273
286
  private canvas!: HTMLCanvasElement;
274
287
  private muteBtn!: HTMLElement;
288
+ private muteSvg!: SVGElement;
275
289
  private stopBtn!: HTMLElement;
276
290
  private visualizerWrap!: HTMLElement;
277
291
 
@@ -361,6 +375,7 @@ export class ConversationalPanel {
361
375
  this.timerEl = this.el.querySelector('.cv-timer')!;
362
376
  this.canvas = this.el.querySelector('.cv-canvas')!;
363
377
  this.muteBtn = this.el.querySelector('.cv-btn--mute')!;
378
+ this.muteSvg = this.muteBtn.querySelector('svg')!;
364
379
  this.stopBtn = this.el.querySelector('.cv-btn--end')!;
365
380
  this.visualizerWrap = this.el.querySelector('.cv-visualizer-wrap')!;
366
381
 
@@ -379,6 +394,10 @@ export class ConversationalPanel {
379
394
  /** Start the conversation (called when user clicks "Start Conversation" or externally) */
380
395
  async start() {
381
396
  if (this.isRunning) return;
397
+ if (this._locked) {
398
+ this.showError('This session has ended. Refresh the page to start a new one.');
399
+ return;
400
+ }
382
401
  this.isRunning = true;
383
402
  this.startBtn.disabled = true;
384
403
  this.errorEl.classList.remove('is-visible');
@@ -417,6 +436,7 @@ export class ConversationalPanel {
417
436
  this.onTranscription?.(text);
418
437
  },
419
438
  onResponse: (text: string) => {
439
+ this._lastSpeechTime = Date.now();
420
440
  this.onResponse?.(text);
421
441
  },
422
442
  onToolCall: (tool: any) => {
@@ -548,12 +568,30 @@ export class ConversationalPanel {
548
568
 
549
569
  private startTimer() {
550
570
  let elapsed = 0;
571
+ const maxDur = this.cfg.maxDuration || 300;
572
+ const silentMax = this.cfg.silenceTimeout || 30;
573
+ this._lastSpeechTime = Date.now();
551
574
  this.timerEl.textContent = '00:00';
552
575
  this.timerTicker = window.setInterval(() => {
553
576
  elapsed++;
554
577
  const m = Math.floor(elapsed / 60).toString().padStart(2, '0');
555
578
  const s = (elapsed % 60).toString().padStart(2, '0');
556
579
  this.timerEl.textContent = `${m}:${s}`;
580
+
581
+ // Check max duration
582
+ if (elapsed >= maxDur) {
583
+ this._locked = true;
584
+ this.stop();
585
+ this.showError('Conversation time limit reached. Refresh to start a new one.');
586
+ return;
587
+ }
588
+
589
+ // Check silence timeout
590
+ if (Date.now() - this._lastSpeechTime > silentMax * 1000) {
591
+ this._locked = true;
592
+ this.stop();
593
+ this.showError('Session ended due to inactivity. Refresh to start a new one.');
594
+ }
557
595
  }, 1000);
558
596
  }
559
597
 
@@ -564,12 +602,10 @@ export class ConversationalPanel {
564
602
  this.muteBtn.classList.toggle('is-muted', muted);
565
603
  const span = this.muteBtn.querySelector('span');
566
604
  if (span) span.textContent = muted ? 'Unmute' : 'Mute';
567
- if (muted) {
568
- this.agent.pauseAudio?.();
569
- this.visualizer?.setDeactivated?.(true);
570
- } else {
571
- this.agent.resumeAudio?.();
572
- this.visualizer?.setDeactivated?.(false);
605
+ if (this.muteSvg) {
606
+ this.muteSvg.innerHTML = muted
607
+ ? '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="23" y1="1" x2="1" y2="23"/>'
608
+ : '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/>';
573
609
  }
574
610
  } catch (_) {}
575
611
  }