@nextpa/chat-core 0.1.0
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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/constants/defaults.d.ts +6 -0
- package/dist/constants/defaults.js +69 -0
- package/dist/core/chat-manager.d.ts +17 -0
- package/dist/core/chat-manager.js +39 -0
- package/dist/core/config-manager.d.ts +14 -0
- package/dist/core/config-manager.js +75 -0
- package/dist/core/connection-manager.d.ts +68 -0
- package/dist/core/connection-manager.js +273 -0
- package/dist/core/connection-retry-handler.d.ts +11 -0
- package/dist/core/connection-retry-handler.js +33 -0
- package/dist/core/consent-manager.d.ts +15 -0
- package/dist/core/consent-manager.js +40 -0
- package/dist/core/conversation-persistence.d.ts +23 -0
- package/dist/core/conversation-persistence.js +109 -0
- package/dist/core/event-emitter.d.ts +7 -0
- package/dist/core/event-emitter.js +27 -0
- package/dist/core/greeting-gate.d.ts +16 -0
- package/dist/core/greeting-gate.js +53 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +7 -0
- package/dist/core/link-target.d.ts +15 -0
- package/dist/core/link-target.js +64 -0
- package/dist/core/local-storage-persistence.d.ts +11 -0
- package/dist/core/local-storage-persistence.js +111 -0
- package/dist/core/navigation-manager.d.ts +24 -0
- package/dist/core/navigation-manager.js +132 -0
- package/dist/core/popover-manager.d.ts +14 -0
- package/dist/core/popover-manager.js +36 -0
- package/dist/core/session-beacon.d.ts +11 -0
- package/dist/core/session-beacon.js +60 -0
- package/dist/core/text-connection-manager.d.ts +58 -0
- package/dist/core/text-connection-manager.js +200 -0
- package/dist/core/text-mode-manager.d.ts +50 -0
- package/dist/core/text-mode-manager.js +154 -0
- package/dist/core/text-types.d.ts +20 -0
- package/dist/core/text-types.js +1 -0
- package/dist/core/text-widget-controller.d.ts +47 -0
- package/dist/core/text-widget-controller.js +278 -0
- package/dist/core/theme-manager.d.ts +10 -0
- package/dist/core/theme-manager.js +25 -0
- package/dist/core/types.d.ts +50 -0
- package/dist/core/types.js +1 -0
- package/dist/core/voice-state-manager.d.ts +85 -0
- package/dist/core/voice-state-manager.js +317 -0
- package/dist/core/widget-controller.d.ts +58 -0
- package/dist/core/widget-controller.js +533 -0
- package/dist/core/widget-core.d.ts +51 -0
- package/dist/core/widget-core.js +160 -0
- package/dist/google.d.ts +1 -0
- package/dist/google.js +12 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +40 -0
- package/dist/openai.d.ts +1 -0
- package/dist/openai.js +7 -0
- package/dist/services/api.d.ts +114 -0
- package/dist/services/api.js +336 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/page-interaction/constants.d.ts +1 -0
- package/dist/services/page-interaction/constants.js +1 -0
- package/dist/services/page-interaction/content-extractor.d.ts +2 -0
- package/dist/services/page-interaction/content-extractor.js +104 -0
- package/dist/services/page-interaction/deep-query.d.ts +8 -0
- package/dist/services/page-interaction/deep-query.js +44 -0
- package/dist/services/page-interaction/dom-executor.d.ts +7 -0
- package/dist/services/page-interaction/dom-executor.js +172 -0
- package/dist/services/page-interaction/dom-extractor.d.ts +8 -0
- package/dist/services/page-interaction/dom-extractor.js +252 -0
- package/dist/services/page-interaction/extension-bridge.d.ts +10 -0
- package/dist/services/page-interaction/extension-bridge.js +86 -0
- package/dist/services/page-interaction/iframe-navigation-manager.d.ts +28 -0
- package/dist/services/page-interaction/iframe-navigation-manager.js +154 -0
- package/dist/services/page-interaction/index.d.ts +5 -0
- package/dist/services/page-interaction/index.js +4 -0
- package/dist/services/page-interaction/page-interaction-handler.d.ts +21 -0
- package/dist/services/page-interaction/page-interaction-handler.js +129 -0
- package/dist/services/page-interaction/selector-generator.d.ts +1 -0
- package/dist/services/page-interaction/selector-generator.js +48 -0
- package/dist/services/page-interaction/types.d.ts +75 -0
- package/dist/services/page-interaction/types.js +1 -0
- package/dist/services/realtime/google/google-audio.d.ts +44 -0
- package/dist/services/realtime/google/google-audio.js +410 -0
- package/dist/services/realtime/google/google-provider.d.ts +47 -0
- package/dist/services/realtime/google/google-provider.js +465 -0
- package/dist/services/realtime/google/google-tool-mapper.d.ts +3 -0
- package/dist/services/realtime/google/google-tool-mapper.js +70 -0
- package/dist/services/realtime/google/index.d.ts +1 -0
- package/dist/services/realtime/google/index.js +1 -0
- package/dist/services/realtime/openai/index.d.ts +1 -0
- package/dist/services/realtime/openai/index.js +1 -0
- package/dist/services/realtime/openai/openai-audio-monitor.d.ts +1 -0
- package/dist/services/realtime/openai/openai-audio-monitor.js +55 -0
- package/dist/services/realtime/openai/openai-provider.d.ts +50 -0
- package/dist/services/realtime/openai/openai-provider.js +579 -0
- package/dist/services/realtime/openai/openai-tool-mapper.d.ts +15 -0
- package/dist/services/realtime/openai/openai-tool-mapper.js +14 -0
- package/dist/services/realtime/provider-factory.d.ts +4 -0
- package/dist/services/realtime/provider-factory.js +12 -0
- package/dist/services/realtime/types.d.ts +78 -0
- package/dist/services/realtime/types.js +1 -0
- package/dist/services/realtime/vertexai/index.d.ts +1 -0
- package/dist/services/realtime/vertexai/index.js +1 -0
- package/dist/services/realtime/vertexai/vertexai-provider.d.ts +53 -0
- package/dist/services/realtime/vertexai/vertexai-provider.js +561 -0
- package/dist/services/realtime/voice-session-tracker.d.ts +10 -0
- package/dist/services/realtime/voice-session-tracker.js +62 -0
- package/dist/services/signalr.d.ts +37 -0
- package/dist/services/signalr.js +145 -0
- package/dist/services/thinking-audio-pulse.d.ts +2 -0
- package/dist/services/thinking-audio-pulse.js +72 -0
- package/dist/services/thinking-audio.d.ts +2 -0
- package/dist/services/thinking-audio.js +105 -0
- package/dist/services/voice-health-watchdog.d.ts +42 -0
- package/dist/services/voice-health-watchdog.js +90 -0
- package/dist/services/webrtc-event-handler.d.ts +4 -0
- package/dist/services/webrtc-event-handler.js +42 -0
- package/dist/services/webrtc-greeting.d.ts +3 -0
- package/dist/services/webrtc-greeting.js +62 -0
- package/dist/services/webrtc-microphone.d.ts +15 -0
- package/dist/services/webrtc-microphone.js +64 -0
- package/dist/services/webrtc.d.ts +44 -0
- package/dist/services/webrtc.js +196 -0
- package/dist/types/index.d.ts +151 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/audio-context.d.ts +11 -0
- package/dist/utils/audio-context.js +33 -0
- package/dist/utils/connect-chime.d.ts +11 -0
- package/dist/utils/connect-chime.js +230 -0
- package/dist/utils/consent.d.ts +4 -0
- package/dist/utils/consent.js +30 -0
- package/dist/utils/language-detection.d.ts +1 -0
- package/dist/utils/language-detection.js +17 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/mic-level-analyser.d.ts +13 -0
- package/dist/utils/mic-level-analyser.js +50 -0
- package/dist/utils/proactive-events.d.ts +5 -0
- package/dist/utils/proactive-events.js +1 -0
- package/dist/utils/styles.d.ts +4 -0
- package/dist/utils/styles.js +35 -0
- package/dist/utils/theme-detection.d.ts +3 -0
- package/dist/utils/theme-detection.js +121 -0
- package/dist/utils/tool-status.d.ts +10 -0
- package/dist/utils/tool-status.js +61 -0
- package/dist/utils/ui-sounds.d.ts +3 -0
- package/dist/utils/ui-sounds.js +57 -0
- package/package.json +56 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
import { IframeNavigationManager } from '../services/page-interaction/iframe-navigation-manager';
|
|
3
|
+
import { resolveSameOriginTarget } from '../core/link-target';
|
|
4
|
+
const RECONNECT_MAX_AGE_MS = 30_000;
|
|
5
|
+
const RECONNECT_STORAGE_KEY = 'nextpa-widget-reconnect';
|
|
6
|
+
export class NavigationManager {
|
|
7
|
+
_blockedUrl = null;
|
|
8
|
+
iframeNavManager;
|
|
9
|
+
onStateChanged;
|
|
10
|
+
onDocumentProviderChanged;
|
|
11
|
+
getPageHost;
|
|
12
|
+
get blockedUrl() {
|
|
13
|
+
return this._blockedUrl;
|
|
14
|
+
}
|
|
15
|
+
constructor(onStateChanged, onDocumentProviderChanged, getPageHost) {
|
|
16
|
+
this.iframeNavManager = new IframeNavigationManager();
|
|
17
|
+
this.onStateChanged = onStateChanged;
|
|
18
|
+
this.onDocumentProviderChanged = onDocumentProviderChanged;
|
|
19
|
+
this.getPageHost = getPageHost;
|
|
20
|
+
}
|
|
21
|
+
resolveTargetForUrl(url) {
|
|
22
|
+
return resolveSameOriginTarget(url, this.getPageHost());
|
|
23
|
+
}
|
|
24
|
+
checkForReconnect() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = sessionStorage.getItem(RECONNECT_STORAGE_KEY);
|
|
27
|
+
if (!raw)
|
|
28
|
+
return false;
|
|
29
|
+
sessionStorage.removeItem(RECONNECT_STORAGE_KEY);
|
|
30
|
+
const data = JSON.parse(raw);
|
|
31
|
+
const age = Date.now() - data.timestamp;
|
|
32
|
+
if (age > RECONNECT_MAX_AGE_MS)
|
|
33
|
+
return false;
|
|
34
|
+
logger.debug('[NavigationManager] Fast reconnect after navigation fallback');
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
openUrl(url) {
|
|
42
|
+
const target = this.resolveTargetForUrl(url);
|
|
43
|
+
logger.debug('[NavigationManager] Opening URL:', url, '| target:', target);
|
|
44
|
+
if (target === '_self') {
|
|
45
|
+
this.navigateSameTab(url);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.openInNewTab(url);
|
|
49
|
+
}
|
|
50
|
+
dismissBlockedUrl() {
|
|
51
|
+
this._blockedUrl = null;
|
|
52
|
+
this.onStateChanged();
|
|
53
|
+
}
|
|
54
|
+
openBlockedUrl() {
|
|
55
|
+
if (!this._blockedUrl)
|
|
56
|
+
return;
|
|
57
|
+
const url = this._blockedUrl;
|
|
58
|
+
this._blockedUrl = null;
|
|
59
|
+
this.onStateChanged();
|
|
60
|
+
if (this.resolveTargetForUrl(url) === '_self') {
|
|
61
|
+
this.navigateSameTab(url);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.openInNewTab(url);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
dispose() {
|
|
68
|
+
this.iframeNavManager.close();
|
|
69
|
+
}
|
|
70
|
+
navigateSameTab(url) {
|
|
71
|
+
const event = new CustomEvent('nextpa:navigate', {
|
|
72
|
+
detail: { url },
|
|
73
|
+
bubbles: true,
|
|
74
|
+
composed: true,
|
|
75
|
+
cancelable: true,
|
|
76
|
+
});
|
|
77
|
+
// Customer widget: the host listens on the <nextpa-widget> element. In-page hosts
|
|
78
|
+
// (the in-shell assistant) have no such element, so fall back to dispatching on the
|
|
79
|
+
// document — a host listener can preventDefault to take over navigation (e.g. a Blazor
|
|
80
|
+
// SPA route change) instead of the iframe-overlay / full-reload fallback.
|
|
81
|
+
const target = document.querySelector('nextpa-widget') ?? document;
|
|
82
|
+
const handled = !target.dispatchEvent(event);
|
|
83
|
+
if (handled)
|
|
84
|
+
return;
|
|
85
|
+
this.navigateViaIframeWithFallback(url);
|
|
86
|
+
}
|
|
87
|
+
navigateViaIframeWithFallback(url) {
|
|
88
|
+
const wasAlreadyActive = this.iframeNavManager.isActive();
|
|
89
|
+
this.iframeNavManager.navigateTo(url).then((result) => {
|
|
90
|
+
if (result.status === 'iframe-loaded') {
|
|
91
|
+
logger.debug('[NavigationManager] Navigation loaded in iframe:', url);
|
|
92
|
+
const getDoc = () => this.iframeNavManager.getTargetDocument();
|
|
93
|
+
const getWin = () => this.iframeNavManager.getTargetWindow();
|
|
94
|
+
this.onDocumentProviderChanged(getDoc, getWin);
|
|
95
|
+
if (!wasAlreadyActive) {
|
|
96
|
+
this.iframeNavManager.onClose(() => {
|
|
97
|
+
this.onDocumentProviderChanged(() => document, () => globalThis);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
logger.debug('[NavigationManager] Iframe blocked, falling back to full navigation:', result.reason);
|
|
103
|
+
this.saveStateForReconnect();
|
|
104
|
+
globalThis.location.href = url;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
openInNewTab(url) {
|
|
108
|
+
try {
|
|
109
|
+
const win = globalThis.open(url, '_blank');
|
|
110
|
+
if (win !== null)
|
|
111
|
+
return;
|
|
112
|
+
logger.warn('[NavigationManager] Popup blocked:', url);
|
|
113
|
+
this.showBlockedUrl(url);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
logger.warn('[NavigationManager] Failed to open:', url);
|
|
117
|
+
this.showBlockedUrl(url);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
showBlockedUrl(url) {
|
|
121
|
+
this._blockedUrl = url;
|
|
122
|
+
this.onStateChanged();
|
|
123
|
+
}
|
|
124
|
+
saveStateForReconnect() {
|
|
125
|
+
try {
|
|
126
|
+
sessionStorage.setItem(RECONNECT_STORAGE_KEY, JSON.stringify({
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
catch { /* private browsing or storage full */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WidgetConfig } from '../types';
|
|
2
|
+
type StateChangeCallback = () => void;
|
|
3
|
+
export declare class PopoverManager {
|
|
4
|
+
private _showPopover;
|
|
5
|
+
private timerId;
|
|
6
|
+
private readonly onStateChanged;
|
|
7
|
+
get showPopover(): boolean;
|
|
8
|
+
constructor(onStateChanged: StateChangeCallback);
|
|
9
|
+
initialize(config: WidgetConfig): void;
|
|
10
|
+
dismiss(): void;
|
|
11
|
+
dispose(): void;
|
|
12
|
+
private clearTimer;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const DEFAULT_AUTO_CLOSE_SECONDS = 5;
|
|
2
|
+
const MS_PER_SECOND = 1000;
|
|
3
|
+
export class PopoverManager {
|
|
4
|
+
_showPopover = false;
|
|
5
|
+
timerId = null;
|
|
6
|
+
onStateChanged;
|
|
7
|
+
get showPopover() {
|
|
8
|
+
return this._showPopover;
|
|
9
|
+
}
|
|
10
|
+
constructor(onStateChanged) {
|
|
11
|
+
this.onStateChanged = onStateChanged;
|
|
12
|
+
}
|
|
13
|
+
initialize(config) {
|
|
14
|
+
if (!config.popoverEnabled || !config.popoverText)
|
|
15
|
+
return;
|
|
16
|
+
this._showPopover = true;
|
|
17
|
+
const autoCloseSeconds = config.popoverAutoCloseSeconds ?? DEFAULT_AUTO_CLOSE_SECONDS;
|
|
18
|
+
this.timerId = setTimeout(() => this.dismiss(), autoCloseSeconds * MS_PER_SECOND);
|
|
19
|
+
}
|
|
20
|
+
dismiss() {
|
|
21
|
+
if (!this._showPopover)
|
|
22
|
+
return;
|
|
23
|
+
this._showPopover = false;
|
|
24
|
+
this.clearTimer();
|
|
25
|
+
this.onStateChanged();
|
|
26
|
+
}
|
|
27
|
+
dispose() {
|
|
28
|
+
this.clearTimer();
|
|
29
|
+
}
|
|
30
|
+
clearTimer() {
|
|
31
|
+
if (!this.timerId)
|
|
32
|
+
return;
|
|
33
|
+
clearTimeout(this.timerId);
|
|
34
|
+
this.timerId = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class SessionBeacon {
|
|
2
|
+
private beaconUrl;
|
|
3
|
+
private beaconPayload;
|
|
4
|
+
private registered;
|
|
5
|
+
private sent;
|
|
6
|
+
private onSessionResumed;
|
|
7
|
+
private readonly handleVisibilityChange;
|
|
8
|
+
register(baseUrl: string, clientId: string, assistantId: string, onSessionResumed: () => void): void;
|
|
9
|
+
unregister(): void;
|
|
10
|
+
private sendBeacon;
|
|
11
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
const LOG_PREFIX = '[SessionBeacon]';
|
|
3
|
+
export class SessionBeacon {
|
|
4
|
+
beaconUrl = null;
|
|
5
|
+
beaconPayload = null;
|
|
6
|
+
registered = false;
|
|
7
|
+
sent = false;
|
|
8
|
+
onSessionResumed = null;
|
|
9
|
+
handleVisibilityChange = () => {
|
|
10
|
+
if (document.visibilityState === 'hidden') {
|
|
11
|
+
this.sendBeacon();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (this.sent) {
|
|
15
|
+
this.sent = false;
|
|
16
|
+
logger.debug(`${LOG_PREFIX} Tab returned after beacon, resuming session`);
|
|
17
|
+
this.onSessionResumed?.();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
register(baseUrl, clientId, assistantId, onSessionResumed) {
|
|
21
|
+
if (this.registered)
|
|
22
|
+
return;
|
|
23
|
+
this.beaconUrl = `${baseUrl}/api/widget/conversation/end-beacon`;
|
|
24
|
+
// tenantId is no longer sent — the server derives it from the Assistant aggregate.
|
|
25
|
+
this.beaconPayload = JSON.stringify({ clientId, assistantId });
|
|
26
|
+
this.onSessionResumed = onSessionResumed;
|
|
27
|
+
this.registered = true;
|
|
28
|
+
this.sent = false;
|
|
29
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
30
|
+
logger.debug(`${LOG_PREFIX} Registered beacon`);
|
|
31
|
+
}
|
|
32
|
+
unregister() {
|
|
33
|
+
if (!this.registered)
|
|
34
|
+
return;
|
|
35
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
36
|
+
this.beaconUrl = null;
|
|
37
|
+
this.beaconPayload = null;
|
|
38
|
+
this.onSessionResumed = null;
|
|
39
|
+
this.registered = false;
|
|
40
|
+
this.sent = false;
|
|
41
|
+
logger.debug(`${LOG_PREFIX} Unregistered beacon`);
|
|
42
|
+
}
|
|
43
|
+
sendBeacon() {
|
|
44
|
+
if (!this.beaconUrl || !this.beaconPayload || this.sent)
|
|
45
|
+
return;
|
|
46
|
+
this.sent = true;
|
|
47
|
+
fetch(this.beaconUrl, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: this.beaconPayload,
|
|
51
|
+
keepalive: true,
|
|
52
|
+
}).catch(() => {
|
|
53
|
+
if (this.beaconUrl && this.beaconPayload) {
|
|
54
|
+
const blob = new Blob([this.beaconPayload], { type: 'application/json' });
|
|
55
|
+
navigator.sendBeacon(this.beaconUrl, blob);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
logger.debug(`${LOG_PREFIX} Beacon sent`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ConnectionState } from '../types';
|
|
2
|
+
import type { WidgetApiClient } from '../services/api';
|
|
3
|
+
import { type DocumentProvider, type WindowProvider, type ExternalExecutor } from '../services/page-interaction/page-interaction-handler';
|
|
4
|
+
import { EventEmitter } from './event-emitter';
|
|
5
|
+
interface TextStreamDelta {
|
|
6
|
+
readonly delta: string;
|
|
7
|
+
readonly messageId: string;
|
|
8
|
+
}
|
|
9
|
+
interface TextStreamCompleted {
|
|
10
|
+
readonly fullText: string;
|
|
11
|
+
readonly messageId: string;
|
|
12
|
+
}
|
|
13
|
+
interface TextToolCall {
|
|
14
|
+
readonly toolName: string;
|
|
15
|
+
readonly status: string;
|
|
16
|
+
readonly result?: string;
|
|
17
|
+
readonly agentLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
interface TextConnectionEvents {
|
|
20
|
+
connectionStateChanged: (state: ConnectionState) => void;
|
|
21
|
+
streamDelta: (data: TextStreamDelta) => void;
|
|
22
|
+
streamCompleted: (data: TextStreamCompleted) => void;
|
|
23
|
+
toolCall: (data: TextToolCall) => void;
|
|
24
|
+
error: (error: string) => void;
|
|
25
|
+
stopConversation: () => void;
|
|
26
|
+
openUrl: (url: string) => void;
|
|
27
|
+
openFile: (url: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare class TextConnectionManager extends EventEmitter<TextConnectionEvents> {
|
|
30
|
+
private signalRService;
|
|
31
|
+
private pageInteractionHandler;
|
|
32
|
+
private readonly apiClient;
|
|
33
|
+
private connectionState;
|
|
34
|
+
private serviceInitialized;
|
|
35
|
+
private pendingExternalExecutor;
|
|
36
|
+
private pendingDocumentProvider;
|
|
37
|
+
private greetingActive;
|
|
38
|
+
private pendingSends;
|
|
39
|
+
private greetingHoldTimer;
|
|
40
|
+
constructor(apiClient: WidgetApiClient);
|
|
41
|
+
private initializeService;
|
|
42
|
+
private setupEventHandlers;
|
|
43
|
+
connect(): Promise<void>;
|
|
44
|
+
private setupTextChatHandlers;
|
|
45
|
+
private setupPageInteractionHandler;
|
|
46
|
+
private simulateDemoConnection;
|
|
47
|
+
sendMessage(text: string, pageUrl?: string, pageTitle?: string, isGreeting?: boolean): Promise<void>;
|
|
48
|
+
sendGreeting(pageUrl?: string, pageTitle?: string): Promise<void>;
|
|
49
|
+
private beginGreetingHold;
|
|
50
|
+
private releaseGreetingHold;
|
|
51
|
+
disconnect(): Promise<void>;
|
|
52
|
+
setDocumentProvider(getDoc: DocumentProvider, getWin: WindowProvider): void;
|
|
53
|
+
setExternalExecutor(executor: ExternalExecutor | null): void;
|
|
54
|
+
getConnectionState(): ConnectionState;
|
|
55
|
+
isConnected(): boolean;
|
|
56
|
+
dispose(): void;
|
|
57
|
+
}
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { PageInteractionHandler } from '../services/page-interaction/page-interaction-handler';
|
|
2
|
+
import { isDemoMode } from '../constants/defaults';
|
|
3
|
+
import { logger } from '../utils/logger';
|
|
4
|
+
import { EventEmitter } from './event-emitter';
|
|
5
|
+
const DEMO_CONNECTION_DELAY_MS = 300;
|
|
6
|
+
// Safety cap: if a greeting never streams (error / no response), release held user messages anyway.
|
|
7
|
+
const GREETING_HOLD_TIMEOUT_MS = 15_000;
|
|
8
|
+
export class TextConnectionManager extends EventEmitter {
|
|
9
|
+
signalRService = null;
|
|
10
|
+
pageInteractionHandler = null;
|
|
11
|
+
apiClient;
|
|
12
|
+
connectionState = 'disconnected';
|
|
13
|
+
serviceInitialized = false;
|
|
14
|
+
pendingExternalExecutor = null;
|
|
15
|
+
pendingDocumentProvider = null;
|
|
16
|
+
// While the greeting streams, user messages are held (like voice keeps the mic muted until the
|
|
17
|
+
// greeting audio ends) and flushed once the greeting completes.
|
|
18
|
+
greetingActive = false;
|
|
19
|
+
pendingSends = [];
|
|
20
|
+
greetingHoldTimer = null;
|
|
21
|
+
constructor(apiClient) {
|
|
22
|
+
super();
|
|
23
|
+
this.apiClient = apiClient;
|
|
24
|
+
}
|
|
25
|
+
async initializeService() {
|
|
26
|
+
if (this.serviceInitialized) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
logger.debug('[TextConnectionManager] Lazy loading SignalR service...');
|
|
30
|
+
const { SignalRService } = await import('../services/signalr');
|
|
31
|
+
this.signalRService = new SignalRService();
|
|
32
|
+
this.setupEventHandlers();
|
|
33
|
+
this.serviceInitialized = true;
|
|
34
|
+
logger.debug('[TextConnectionManager] Service initialized');
|
|
35
|
+
}
|
|
36
|
+
setupEventHandlers() {
|
|
37
|
+
if (!this.signalRService) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.signalRService.setEventHandlers({
|
|
41
|
+
onStopConversation: () => this.emit('stopConversation'),
|
|
42
|
+
onOpenUrl: (url) => this.emit('openUrl', url),
|
|
43
|
+
onOpenFile: (url) => this.emit('openFile', url),
|
|
44
|
+
onError: (title, message) => this.emit('error', `${title}: ${message}`),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async connect() {
|
|
48
|
+
if (isDemoMode) {
|
|
49
|
+
await this.simulateDemoConnection();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await this.initializeService();
|
|
53
|
+
if (!this.signalRService) {
|
|
54
|
+
throw new Error('SignalR service not initialized');
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
this.connectionState = 'connecting';
|
|
58
|
+
this.emit('connectionStateChanged', 'connecting');
|
|
59
|
+
const bearerToken = this.apiClient.getBearerToken();
|
|
60
|
+
if (!bearerToken) {
|
|
61
|
+
throw new Error('Not authenticated');
|
|
62
|
+
}
|
|
63
|
+
const claims = this.apiClient.getTokenClaims();
|
|
64
|
+
this.signalRService.setRegistrationInfo(this.apiClient.getClientId(), claims.tenantId ?? '00000000-0000-0000-0000-000000000000', claims.userId ?? '00000000-0000-0000-0000-000000000000');
|
|
65
|
+
this.setupTextChatHandlers(this.signalRService);
|
|
66
|
+
this.setupPageInteractionHandler(this.signalRService);
|
|
67
|
+
await this.signalRService.connect(this.apiClient.getBaseUrl(), bearerToken);
|
|
68
|
+
this.connectionState = 'connected';
|
|
69
|
+
this.emit('connectionStateChanged', 'connected');
|
|
70
|
+
logger.debug('[TextConnectionManager] Connected successfully');
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.connectionState = 'disconnected';
|
|
74
|
+
const message = err instanceof Error ? err.message : 'Connection failed';
|
|
75
|
+
logger.error('[TextConnectionManager] Connection failed:', message);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
setupTextChatHandlers(signalR) {
|
|
80
|
+
signalR.setTextChatHandlers({
|
|
81
|
+
onStreamDelta: (delta, messageId) => {
|
|
82
|
+
this.emit('streamDelta', { delta, messageId });
|
|
83
|
+
},
|
|
84
|
+
onStreamCompleted: (fullText, messageId) => {
|
|
85
|
+
// Release the transport hold BEFORE notifying, so a controller replaying a held
|
|
86
|
+
// message from its own streamCompleted handler sends it straight through.
|
|
87
|
+
this.releaseGreetingHold();
|
|
88
|
+
this.emit('streamCompleted', { fullText, messageId });
|
|
89
|
+
},
|
|
90
|
+
onToolCall: (toolName, status, result, agentLabel) => {
|
|
91
|
+
this.emit('toolCall', { toolName, status, result, agentLabel });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
setupPageInteractionHandler(signalR) {
|
|
96
|
+
this.pageInteractionHandler = new PageInteractionHandler(() => signalR.getConnection() ?? null);
|
|
97
|
+
if (this.pendingExternalExecutor) {
|
|
98
|
+
this.pageInteractionHandler.setExternalExecutor(this.pendingExternalExecutor);
|
|
99
|
+
}
|
|
100
|
+
if (this.pendingDocumentProvider) {
|
|
101
|
+
this.pageInteractionHandler.setDocumentProvider(this.pendingDocumentProvider.getDoc, this.pendingDocumentProvider.getWin);
|
|
102
|
+
}
|
|
103
|
+
signalR.setPageInteractionHandlers({
|
|
104
|
+
onPageInteractionRequest: (requestJson) => {
|
|
105
|
+
this.pageInteractionHandler?.handleRequest(requestJson);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async simulateDemoConnection() {
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, DEMO_CONNECTION_DELAY_MS));
|
|
111
|
+
this.connectionState = 'connected';
|
|
112
|
+
this.emit('connectionStateChanged', 'connected');
|
|
113
|
+
}
|
|
114
|
+
async sendMessage(text, pageUrl, pageTitle, isGreeting = false) {
|
|
115
|
+
if (isDemoMode) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Hold user messages until the greeting has finished streaming (mirrors voice, where the mic
|
|
119
|
+
// stays muted until the greeting audio ends). The greeting itself is never held.
|
|
120
|
+
if (!isGreeting && this.greetingActive) {
|
|
121
|
+
this.pendingSends.push({ text, pageUrl, pageTitle });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await this.apiClient.sendChatMessage(text, pageUrl, pageTitle, isGreeting);
|
|
125
|
+
}
|
|
126
|
+
// The greeting is an empty message flagged as a greeting: the backend primes the LLM to speak
|
|
127
|
+
// first and its empty-message guard only lets the message through when this flag is set. Callers
|
|
128
|
+
// must use this rather than sendMessage('') so the flag can never be forgotten.
|
|
129
|
+
async sendGreeting(pageUrl, pageTitle) {
|
|
130
|
+
this.beginGreetingHold();
|
|
131
|
+
try {
|
|
132
|
+
await this.sendMessage('', pageUrl, pageTitle, true);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// The greeting will never stream, so don't strand held user messages.
|
|
136
|
+
this.releaseGreetingHold();
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
beginGreetingHold() {
|
|
141
|
+
this.greetingActive = true;
|
|
142
|
+
if (this.greetingHoldTimer) {
|
|
143
|
+
clearTimeout(this.greetingHoldTimer);
|
|
144
|
+
}
|
|
145
|
+
this.greetingHoldTimer = setTimeout(() => this.releaseGreetingHold(), GREETING_HOLD_TIMEOUT_MS);
|
|
146
|
+
}
|
|
147
|
+
releaseGreetingHold() {
|
|
148
|
+
if (this.greetingHoldTimer) {
|
|
149
|
+
clearTimeout(this.greetingHoldTimer);
|
|
150
|
+
this.greetingHoldTimer = null;
|
|
151
|
+
}
|
|
152
|
+
if (!this.greetingActive) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.greetingActive = false;
|
|
156
|
+
const queued = this.pendingSends;
|
|
157
|
+
this.pendingSends = [];
|
|
158
|
+
for (const send of queued) {
|
|
159
|
+
void this.sendMessage(send.text, send.pageUrl, send.pageTitle);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async disconnect() {
|
|
163
|
+
this.greetingActive = false;
|
|
164
|
+
this.pendingSends = [];
|
|
165
|
+
if (this.greetingHoldTimer) {
|
|
166
|
+
clearTimeout(this.greetingHoldTimer);
|
|
167
|
+
this.greetingHoldTimer = null;
|
|
168
|
+
}
|
|
169
|
+
if (isDemoMode) {
|
|
170
|
+
this.connectionState = 'disconnected';
|
|
171
|
+
this.emit('connectionStateChanged', 'disconnected');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.signalRService?.clearTextChatHandlers();
|
|
175
|
+
this.signalRService?.clearPageInteractionHandlers();
|
|
176
|
+
await this.signalRService?.disconnect();
|
|
177
|
+
this.pageInteractionHandler = null;
|
|
178
|
+
this.connectionState = 'disconnected';
|
|
179
|
+
this.emit('connectionStateChanged', 'disconnected');
|
|
180
|
+
logger.debug('[TextConnectionManager] Disconnected');
|
|
181
|
+
}
|
|
182
|
+
setDocumentProvider(getDoc, getWin) {
|
|
183
|
+
this.pendingDocumentProvider = { getDoc, getWin };
|
|
184
|
+
this.pageInteractionHandler?.setDocumentProvider(getDoc, getWin);
|
|
185
|
+
}
|
|
186
|
+
setExternalExecutor(executor) {
|
|
187
|
+
this.pendingExternalExecutor = executor;
|
|
188
|
+
this.pageInteractionHandler?.setExternalExecutor(executor);
|
|
189
|
+
}
|
|
190
|
+
getConnectionState() {
|
|
191
|
+
return this.connectionState;
|
|
192
|
+
}
|
|
193
|
+
isConnected() {
|
|
194
|
+
return this.connectionState === 'connected';
|
|
195
|
+
}
|
|
196
|
+
dispose() {
|
|
197
|
+
this.signalRService?.disconnect();
|
|
198
|
+
this.removeAllListeners();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { WidgetApiClient } from '../services/api';
|
|
2
|
+
import type { ConnectionState } from '../types';
|
|
3
|
+
import type { DocumentProvider, WindowProvider, ExternalExecutor } from '../services/page-interaction/page-interaction-handler';
|
|
4
|
+
import type { TextConnectionManager } from './text-connection-manager';
|
|
5
|
+
import type { ChatManager } from './chat-manager';
|
|
6
|
+
import type { ConversationPersistence } from './conversation-persistence';
|
|
7
|
+
import type { NavigationManager } from './navigation-manager';
|
|
8
|
+
type StateChangeCallback = () => void;
|
|
9
|
+
type TextEventDeps = {
|
|
10
|
+
chatManager: ChatManager;
|
|
11
|
+
persistence: ConversationPersistence;
|
|
12
|
+
navigationManager: NavigationManager;
|
|
13
|
+
onError: () => void;
|
|
14
|
+
onStopConversation: () => Promise<void>;
|
|
15
|
+
logPrefix: string;
|
|
16
|
+
};
|
|
17
|
+
export declare class TextModeManager {
|
|
18
|
+
isStreaming: boolean;
|
|
19
|
+
streamContent: string;
|
|
20
|
+
agentStatus: string | null;
|
|
21
|
+
isSessionEnding: boolean;
|
|
22
|
+
private isDelegationActive;
|
|
23
|
+
private language?;
|
|
24
|
+
private connectionManager;
|
|
25
|
+
private chatManager;
|
|
26
|
+
private persistence;
|
|
27
|
+
private logPrefix;
|
|
28
|
+
private currentStreamMessageId;
|
|
29
|
+
private pendingStreamingUpdate;
|
|
30
|
+
private readonly apiClient;
|
|
31
|
+
private readonly onStateChanged;
|
|
32
|
+
private pendingExternalExecutor;
|
|
33
|
+
private readonly greetingGate;
|
|
34
|
+
constructor(apiClient: WidgetApiClient, onStateChanged: StateChangeCallback);
|
|
35
|
+
setLanguage(language?: string): void;
|
|
36
|
+
setWaitForGreeting(waitForGreeting: boolean): void;
|
|
37
|
+
ensureConnection(): Promise<TextConnectionManager>;
|
|
38
|
+
setupEventHandlers(deps: TextEventDeps): void;
|
|
39
|
+
sendMessage(text: string, pageUrl?: string, pageTitle?: string): Promise<void>;
|
|
40
|
+
requestGreeting(pageUrl?: string, pageTitle?: string): void;
|
|
41
|
+
disconnect(): Promise<void>;
|
|
42
|
+
getConnectionState(): ConnectionState;
|
|
43
|
+
setDocumentProvider(getDoc: DocumentProvider, getWin: WindowProvider): void;
|
|
44
|
+
setExternalExecutor(executor: ExternalExecutor | null): void;
|
|
45
|
+
resetStreamState(): void;
|
|
46
|
+
dispose(): void;
|
|
47
|
+
private handleStreamDelta;
|
|
48
|
+
private scheduleStreamingUpdate;
|
|
49
|
+
}
|
|
50
|
+
export {};
|