@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,33 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
const MAX_CONNECT_ATTEMPTS = 3;
|
|
3
|
+
const RETRY_DELAYS_MS = [1000, 3000];
|
|
4
|
+
const FALLBACK_RETRY_DELAY_MS = 3000;
|
|
5
|
+
export class ConnectionRetryHandler {
|
|
6
|
+
async retryAsync({ connectFn, cleanupFn, label, logPrefix, onMicrophoneDenied }) {
|
|
7
|
+
for (let attempt = 1; attempt <= MAX_CONNECT_ATTEMPTS; attempt++) {
|
|
8
|
+
try {
|
|
9
|
+
await connectFn();
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14
|
+
if (message === 'Microphone permission denied') {
|
|
15
|
+
onMicrophoneDenied?.();
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (attempt >= MAX_CONNECT_ATTEMPTS) {
|
|
19
|
+
logger.error(`${logPrefix} ${label} connection failed after ${attempt} attempts: ${message}`);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const delay = RETRY_DELAYS_MS[attempt - 1] ?? FALLBACK_RETRY_DELAY_MS;
|
|
23
|
+
logger.debug(`${logPrefix} ${label} connection attempt ${attempt}/${MAX_CONNECT_ATTEMPTS} failed: ${message}. Retrying in ${delay}ms...`);
|
|
24
|
+
try {
|
|
25
|
+
await cleanupFn();
|
|
26
|
+
}
|
|
27
|
+
catch { /* ignore cleanup errors */ }
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WidgetConfig } from '../types';
|
|
2
|
+
type StateChangeCallback = () => void;
|
|
3
|
+
export declare class ConsentManager {
|
|
4
|
+
private _hasConsented;
|
|
5
|
+
private _showTermsModal;
|
|
6
|
+
private readonly onStateChanged;
|
|
7
|
+
get hasConsented(): boolean;
|
|
8
|
+
get showTermsModal(): boolean;
|
|
9
|
+
constructor(onStateChanged: StateChangeCallback);
|
|
10
|
+
check(assistantId: string, showTermsAndConditions: boolean, autoAccept?: boolean): void;
|
|
11
|
+
requireConsentOrProceed(config: WidgetConfig): boolean;
|
|
12
|
+
accept(assistantId: string): void;
|
|
13
|
+
decline(): void;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { hasUserConsented, setUserConsent } from '../utils/consent';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
export class ConsentManager {
|
|
4
|
+
_hasConsented = false;
|
|
5
|
+
_showTermsModal = false;
|
|
6
|
+
onStateChanged;
|
|
7
|
+
get hasConsented() {
|
|
8
|
+
return this._hasConsented;
|
|
9
|
+
}
|
|
10
|
+
get showTermsModal() {
|
|
11
|
+
return this._showTermsModal;
|
|
12
|
+
}
|
|
13
|
+
constructor(onStateChanged) {
|
|
14
|
+
this.onStateChanged = onStateChanged;
|
|
15
|
+
}
|
|
16
|
+
check(assistantId, showTermsAndConditions, autoAccept = false) {
|
|
17
|
+
if (!showTermsAndConditions || autoAccept) {
|
|
18
|
+
this._hasConsented = true;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this._hasConsented = hasUserConsented(assistantId);
|
|
22
|
+
logger.debug('[ConsentManager] Consent status:', this._hasConsented);
|
|
23
|
+
}
|
|
24
|
+
requireConsentOrProceed(config) {
|
|
25
|
+
if (!config.showTermsAndConditions || this._hasConsented)
|
|
26
|
+
return true;
|
|
27
|
+
this._showTermsModal = true;
|
|
28
|
+
this.onStateChanged();
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
accept(assistantId) {
|
|
32
|
+
setUserConsent(assistantId, true);
|
|
33
|
+
this._hasConsented = true;
|
|
34
|
+
this._showTermsModal = false;
|
|
35
|
+
this.onStateChanged();
|
|
36
|
+
}
|
|
37
|
+
decline() {
|
|
38
|
+
this._showTermsModal = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { VoiceUsage } from '../types';
|
|
2
|
+
import type { WidgetApiClient } from '../services/api';
|
|
3
|
+
import type { ChatManager } from './chat-manager';
|
|
4
|
+
export declare class ConversationPersistence {
|
|
5
|
+
private readonly apiClient;
|
|
6
|
+
private readonly logPrefix;
|
|
7
|
+
private readonly localStorage;
|
|
8
|
+
private assistantId;
|
|
9
|
+
constructor(apiClient: WidgetApiClient, logPrefix: string);
|
|
10
|
+
setAssistantId(assistantId: string): void;
|
|
11
|
+
loadHistory(chatManager: ChatManager): Promise<number>;
|
|
12
|
+
private loadFromLocalStorage;
|
|
13
|
+
private loadFromApi;
|
|
14
|
+
startNewSession(): void;
|
|
15
|
+
endSession(trimFarewell?: boolean): void;
|
|
16
|
+
persistMessage(role: string, content: string, language?: string, usage?: VoiceUsage, toolCalls?: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
succeeded: boolean;
|
|
20
|
+
}>, responseLatencyMs?: number, inputSource?: 'Spoken' | 'Typed'): void;
|
|
21
|
+
saveToLocalStorage(chatManager: ChatManager): void;
|
|
22
|
+
resetMemory(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { isDemoMode } from '../constants/defaults';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
import { LocalStoragePersistence } from './local-storage-persistence';
|
|
4
|
+
export class ConversationPersistence {
|
|
5
|
+
apiClient;
|
|
6
|
+
logPrefix;
|
|
7
|
+
localStorage;
|
|
8
|
+
assistantId = null;
|
|
9
|
+
constructor(apiClient, logPrefix) {
|
|
10
|
+
this.apiClient = apiClient;
|
|
11
|
+
this.logPrefix = logPrefix;
|
|
12
|
+
this.localStorage = new LocalStoragePersistence();
|
|
13
|
+
}
|
|
14
|
+
setAssistantId(assistantId) {
|
|
15
|
+
this.assistantId = assistantId;
|
|
16
|
+
}
|
|
17
|
+
async loadHistory(chatManager) {
|
|
18
|
+
if (isDemoMode)
|
|
19
|
+
return 0;
|
|
20
|
+
const cachedCount = this.loadFromLocalStorage(chatManager);
|
|
21
|
+
if (cachedCount > 0)
|
|
22
|
+
return cachedCount;
|
|
23
|
+
return this.loadFromApi(chatManager);
|
|
24
|
+
}
|
|
25
|
+
loadFromLocalStorage(chatManager) {
|
|
26
|
+
if (!this.assistantId)
|
|
27
|
+
return 0;
|
|
28
|
+
const cached = this.localStorage.load(this.assistantId);
|
|
29
|
+
for (const msg of cached) {
|
|
30
|
+
const added = msg.role === 'user'
|
|
31
|
+
? chatManager.addUserMessage(msg.content, msg.id)
|
|
32
|
+
: chatManager.addAssistantMessage(msg.content, msg.id);
|
|
33
|
+
if (msg.feedback != null) {
|
|
34
|
+
chatManager.setFeedback(added.id, msg.feedback);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (cached.length > 0) {
|
|
38
|
+
logger.debug(`${this.logPrefix} Loaded ${cached.length} messages from localStorage`);
|
|
39
|
+
}
|
|
40
|
+
return cached.length;
|
|
41
|
+
}
|
|
42
|
+
async loadFromApi(chatManager) {
|
|
43
|
+
try {
|
|
44
|
+
const entries = await this.apiClient.getConversationHistory();
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const isUser = entry.role.toLowerCase() === 'user';
|
|
47
|
+
const msg = isUser
|
|
48
|
+
? chatManager.addUserMessage(entry.content, entry.id)
|
|
49
|
+
: chatManager.addAssistantMessage(entry.content, entry.id);
|
|
50
|
+
if (entry.feedback?.rating != null) {
|
|
51
|
+
chatManager.setFeedback(msg.id, entry.feedback.rating);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (entries.length > 0) {
|
|
55
|
+
logger.debug(`${this.logPrefix} Loaded`, entries.length, 'entries from API');
|
|
56
|
+
this.saveToLocalStorage(chatManager);
|
|
57
|
+
}
|
|
58
|
+
return entries.length;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logger.debug(`${this.logPrefix} Failed to load conversation history:`, err);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
startNewSession() {
|
|
66
|
+
if (isDemoMode)
|
|
67
|
+
return;
|
|
68
|
+
this.apiClient.startSession().catch((err) => {
|
|
69
|
+
logger.debug(`${this.logPrefix} Failed to start new session:`, err);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
endSession(trimFarewell = false) {
|
|
73
|
+
if (isDemoMode)
|
|
74
|
+
return;
|
|
75
|
+
// Do NOT clear localStorage — keep messages for next visit
|
|
76
|
+
this.apiClient.endConversationSession(undefined, trimFarewell).catch((err) => {
|
|
77
|
+
logger.debug(`${this.logPrefix} Failed to end conversation session:`, err);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
persistMessage(role, content, language, usage, toolCalls, responseLatencyMs, inputSource) {
|
|
81
|
+
if (isDemoMode)
|
|
82
|
+
return;
|
|
83
|
+
this.apiClient.addConversationMessage({ role, content, language, usage, toolCalls, responseLatencyMs, inputSource }).catch((err) => {
|
|
84
|
+
logger.debug(`${this.logPrefix} Failed to persist message:`, err);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
saveToLocalStorage(chatManager) {
|
|
88
|
+
if (!this.assistantId)
|
|
89
|
+
return;
|
|
90
|
+
this.localStorage.save(this.assistantId, chatManager.getMessages());
|
|
91
|
+
}
|
|
92
|
+
async resetMemory() {
|
|
93
|
+
if (isDemoMode)
|
|
94
|
+
return;
|
|
95
|
+
if (this.assistantId) {
|
|
96
|
+
this.localStorage.clear(this.assistantId);
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await this.apiClient.resetConversation();
|
|
100
|
+
logger.debug(`${this.logPrefix} Conversation memory cleared`);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger.debug(`${this.logPrefix} Failed to reset conversation:`, err);
|
|
104
|
+
}
|
|
105
|
+
// Start the next conversation under a fresh clientId so an in-flight write from the previous
|
|
106
|
+
// turn (processed asynchronously) can't resurrect the just-cleared memory under the same id.
|
|
107
|
+
this.apiClient.rotateClientId();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare class EventEmitter<TEvents = Record<string, (...args: any[]) => void>> {
|
|
2
|
+
private readonly listeners;
|
|
3
|
+
on<K extends keyof TEvents>(event: K, callback: TEvents[K]): () => void;
|
|
4
|
+
off<K extends keyof TEvents>(event: K, callback: TEvents[K]): void;
|
|
5
|
+
protected emit<K extends keyof TEvents>(event: K, ...args: TEvents[K] extends (...args: infer P) => void ? P : never): void;
|
|
6
|
+
removeAllListeners(): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
export class EventEmitter {
|
|
3
|
+
listeners = new Map();
|
|
4
|
+
on(event, callback) {
|
|
5
|
+
if (!this.listeners.has(event)) {
|
|
6
|
+
this.listeners.set(event, new Set());
|
|
7
|
+
}
|
|
8
|
+
this.listeners.get(event).add(callback);
|
|
9
|
+
return () => this.off(event, callback);
|
|
10
|
+
}
|
|
11
|
+
off(event, callback) {
|
|
12
|
+
this.listeners.get(event)?.delete(callback);
|
|
13
|
+
}
|
|
14
|
+
emit(event, ...args) {
|
|
15
|
+
this.listeners.get(event)?.forEach((callback) => {
|
|
16
|
+
try {
|
|
17
|
+
callback(...args);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
logger.error(`Error in event handler for ${String(event)}:`, err);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
removeAllListeners() {
|
|
25
|
+
this.listeners.clear();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class GreetingGate {
|
|
2
|
+
private readonly replay;
|
|
3
|
+
waitForGreeting: boolean;
|
|
4
|
+
private greetingInFlight;
|
|
5
|
+
private held;
|
|
6
|
+
private timer;
|
|
7
|
+
/** `replay` re-runs a held message through the normal send path once the greeting is done. */
|
|
8
|
+
constructor(replay: (text: string) => void);
|
|
9
|
+
beginGreeting(): void;
|
|
10
|
+
/** True while a user message must be fully withheld (neither displayed nor sent yet). */
|
|
11
|
+
shouldHold(): boolean;
|
|
12
|
+
enqueue(text: string): void;
|
|
13
|
+
/** Call when the greeting finishes (or on the safety timeout): replays every withheld message. */
|
|
14
|
+
release(): void;
|
|
15
|
+
reset(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Holds the first user message — both its display and its send — until the greeting has finished
|
|
2
|
+
// streaming, so the greeting always lands before the user's first turn (mirrors voice, where the
|
|
3
|
+
// mic stays muted until the greeting audio ends). Opt out with waitForGreeting = false. Shared by
|
|
4
|
+
// the text-only controller and the combined controller so both greeting paths behave identically.
|
|
5
|
+
const GREETING_HOLD_TIMEOUT_MS = 15_000;
|
|
6
|
+
export class GreetingGate {
|
|
7
|
+
replay;
|
|
8
|
+
waitForGreeting = true;
|
|
9
|
+
greetingInFlight = false;
|
|
10
|
+
held = [];
|
|
11
|
+
timer = null;
|
|
12
|
+
/** `replay` re-runs a held message through the normal send path once the greeting is done. */
|
|
13
|
+
constructor(replay) {
|
|
14
|
+
this.replay = replay;
|
|
15
|
+
}
|
|
16
|
+
beginGreeting() {
|
|
17
|
+
this.greetingInFlight = true;
|
|
18
|
+
if (this.timer) {
|
|
19
|
+
clearTimeout(this.timer);
|
|
20
|
+
}
|
|
21
|
+
this.timer = setTimeout(() => this.release(), GREETING_HOLD_TIMEOUT_MS);
|
|
22
|
+
}
|
|
23
|
+
/** True while a user message must be fully withheld (neither displayed nor sent yet). */
|
|
24
|
+
shouldHold() {
|
|
25
|
+
return this.waitForGreeting && this.greetingInFlight;
|
|
26
|
+
}
|
|
27
|
+
enqueue(text) {
|
|
28
|
+
this.held.push(text);
|
|
29
|
+
}
|
|
30
|
+
/** Call when the greeting finishes (or on the safety timeout): replays every withheld message. */
|
|
31
|
+
release() {
|
|
32
|
+
if (this.timer) {
|
|
33
|
+
clearTimeout(this.timer);
|
|
34
|
+
this.timer = null;
|
|
35
|
+
}
|
|
36
|
+
if (!this.greetingInFlight) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.greetingInFlight = false;
|
|
40
|
+
const queued = this.held.splice(0);
|
|
41
|
+
for (const text of queued) {
|
|
42
|
+
this.replay(text);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
reset() {
|
|
46
|
+
if (this.timer) {
|
|
47
|
+
clearTimeout(this.timer);
|
|
48
|
+
this.timer = null;
|
|
49
|
+
}
|
|
50
|
+
this.greetingInFlight = false;
|
|
51
|
+
this.held = [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { WidgetController } from './widget-controller';
|
|
2
|
+
export { TextWidgetController } from './text-widget-controller';
|
|
3
|
+
export { ConfigManager } from './config-manager';
|
|
4
|
+
export { ChatManager } from './chat-manager';
|
|
5
|
+
export { ConnectionManager } from './connection-manager';
|
|
6
|
+
export { TextConnectionManager } from './text-connection-manager';
|
|
7
|
+
export { EventEmitter } from './event-emitter';
|
|
8
|
+
export type { WidgetState, WidgetControllerOptions, WidgetEvents } from './types';
|
|
9
|
+
export type { TextWidgetState } from './text-types';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { WidgetController } from './widget-controller';
|
|
2
|
+
export { TextWidgetController } from './text-widget-controller';
|
|
3
|
+
export { ConfigManager } from './config-manager';
|
|
4
|
+
export { ChatManager } from './chat-manager';
|
|
5
|
+
export { ConnectionManager } from './connection-manager';
|
|
6
|
+
export { TextConnectionManager } from './text-connection-manager';
|
|
7
|
+
export { EventEmitter } from './event-emitter';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a link clicked through the widget should open in the same
|
|
3
|
+
* tab (`_self`) or a new tab (`_blank`).
|
|
4
|
+
*
|
|
5
|
+
* Rule:
|
|
6
|
+
* - Same domain as the host page → `_self`. `www.` is treated as same domain.
|
|
7
|
+
* Port and protocol differences are ignored — the user-perceived "domain"
|
|
8
|
+
* is the host without `www.`.
|
|
9
|
+
* - Different domain or sub-domain → `_blank`.
|
|
10
|
+
* - Non-http(s) URLs (`mailto:`, `tel:`, relative paths, anchors) → `_self`,
|
|
11
|
+
* because the browser invokes a native handler (mail client, dialer) or
|
|
12
|
+
* stays on the page; the current tab content is not lost.
|
|
13
|
+
* - Unparseable input → `_blank` (safer default).
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveSameOriginTarget(url: string | null | undefined, pageHost: string | null | undefined): '_self' | '_blank';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a link clicked through the widget should open in the same
|
|
3
|
+
* tab (`_self`) or a new tab (`_blank`).
|
|
4
|
+
*
|
|
5
|
+
* Rule:
|
|
6
|
+
* - Same domain as the host page → `_self`. `www.` is treated as same domain.
|
|
7
|
+
* Port and protocol differences are ignored — the user-perceived "domain"
|
|
8
|
+
* is the host without `www.`.
|
|
9
|
+
* - Different domain or sub-domain → `_blank`.
|
|
10
|
+
* - Non-http(s) URLs (`mailto:`, `tel:`, relative paths, anchors) → `_self`,
|
|
11
|
+
* because the browser invokes a native handler (mail client, dialer) or
|
|
12
|
+
* stays on the page; the current tab content is not lost.
|
|
13
|
+
* - Unparseable input → `_blank` (safer default).
|
|
14
|
+
*/
|
|
15
|
+
export function resolveSameOriginTarget(url, pageHost) {
|
|
16
|
+
if (!url)
|
|
17
|
+
return '_blank';
|
|
18
|
+
const trimmed = url.trim();
|
|
19
|
+
if (trimmed.length === 0)
|
|
20
|
+
return '_blank';
|
|
21
|
+
if (isNonHttpScheme(trimmed) || isRelativeOrAnchor(trimmed)) {
|
|
22
|
+
return '_self';
|
|
23
|
+
}
|
|
24
|
+
const linkHost = extractHostname(trimmed);
|
|
25
|
+
if (linkHost === null)
|
|
26
|
+
return '_blank';
|
|
27
|
+
const normalizedLink = stripWww(linkHost);
|
|
28
|
+
const normalizedPage = stripWww(stripPort(pageHost ?? ''));
|
|
29
|
+
if (normalizedPage.length === 0)
|
|
30
|
+
return '_blank';
|
|
31
|
+
return normalizedLink === normalizedPage ? '_self' : '_blank';
|
|
32
|
+
}
|
|
33
|
+
function isNonHttpScheme(url) {
|
|
34
|
+
const lower = url.toLowerCase();
|
|
35
|
+
return lower.startsWith('mailto:')
|
|
36
|
+
|| lower.startsWith('tel:')
|
|
37
|
+
|| lower.startsWith('sms:')
|
|
38
|
+
|| lower.startsWith('geo:')
|
|
39
|
+
|| lower.startsWith('callto:');
|
|
40
|
+
}
|
|
41
|
+
function isRelativeOrAnchor(url) {
|
|
42
|
+
if (url.startsWith('#') || url.startsWith('?'))
|
|
43
|
+
return true;
|
|
44
|
+
if (url.startsWith('//'))
|
|
45
|
+
return false;
|
|
46
|
+
return !/^[a-z][a-z0-9+.-]*:/i.test(url);
|
|
47
|
+
}
|
|
48
|
+
function extractHostname(url) {
|
|
49
|
+
try {
|
|
50
|
+
const absolute = url.startsWith('//') ? `https:${url}` : url;
|
|
51
|
+
return new URL(absolute).hostname.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function stripWww(host) {
|
|
58
|
+
const lower = host.toLowerCase();
|
|
59
|
+
return lower.startsWith('www.') ? lower.slice(4) : lower;
|
|
60
|
+
}
|
|
61
|
+
function stripPort(host) {
|
|
62
|
+
const colonIndex = host.indexOf(':');
|
|
63
|
+
return colonIndex === -1 ? host : host.slice(0, colonIndex);
|
|
64
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChatMessage } from '../types';
|
|
2
|
+
export declare class LocalStoragePersistence {
|
|
3
|
+
private readonly available;
|
|
4
|
+
private readonly ttlMs;
|
|
5
|
+
constructor(ttlHours?: number);
|
|
6
|
+
save(assistantId: string, messages: ChatMessage[]): void;
|
|
7
|
+
load(assistantId: string): ChatMessage[];
|
|
8
|
+
clear(assistantId: string): void;
|
|
9
|
+
private cleanupExpired;
|
|
10
|
+
private getKey;
|
|
11
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
const LOG_PREFIX = '[LocalStoragePersistence]';
|
|
3
|
+
const KEY_PREFIX = 'nextpa_widget_conv_';
|
|
4
|
+
const DEFAULT_TTL_HOURS = 4;
|
|
5
|
+
function isLocalStorageAvailable() {
|
|
6
|
+
try {
|
|
7
|
+
const testKey = '__nextpa_test__';
|
|
8
|
+
localStorage.setItem(testKey, '1');
|
|
9
|
+
localStorage.removeItem(testKey);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class LocalStoragePersistence {
|
|
17
|
+
available;
|
|
18
|
+
ttlMs;
|
|
19
|
+
constructor(ttlHours = DEFAULT_TTL_HOURS) {
|
|
20
|
+
this.available = isLocalStorageAvailable();
|
|
21
|
+
this.ttlMs = ttlHours * 60 * 60 * 1000;
|
|
22
|
+
if (this.available) {
|
|
23
|
+
this.cleanupExpired();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
save(assistantId, messages) {
|
|
27
|
+
if (!this.available || messages.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const key = this.getKey(assistantId);
|
|
31
|
+
const data = {
|
|
32
|
+
messages: messages.map(m => ({
|
|
33
|
+
...m,
|
|
34
|
+
timestamp: m.timestamp instanceof Date ? m.timestamp : new Date(m.timestamp),
|
|
35
|
+
})),
|
|
36
|
+
expiresAt: Date.now() + this.ttlMs,
|
|
37
|
+
};
|
|
38
|
+
localStorage.setItem(key, JSON.stringify(data));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
logger.debug(`${LOG_PREFIX} Failed to save:`, err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
load(assistantId) {
|
|
45
|
+
if (!this.available)
|
|
46
|
+
return [];
|
|
47
|
+
try {
|
|
48
|
+
const key = this.getKey(assistantId);
|
|
49
|
+
const raw = localStorage.getItem(key);
|
|
50
|
+
if (!raw)
|
|
51
|
+
return [];
|
|
52
|
+
const data = JSON.parse(raw);
|
|
53
|
+
if (Date.now() > data.expiresAt) {
|
|
54
|
+
localStorage.removeItem(key);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return data.messages.map(m => ({
|
|
58
|
+
...m,
|
|
59
|
+
timestamp: new Date(m.timestamp),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
logger.debug(`${LOG_PREFIX} Failed to load:`, err);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
clear(assistantId) {
|
|
68
|
+
if (!this.available)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
localStorage.removeItem(this.getKey(assistantId));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Ignore
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
cleanupExpired() {
|
|
78
|
+
try {
|
|
79
|
+
const keysToRemove = [];
|
|
80
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
81
|
+
const key = localStorage.key(i);
|
|
82
|
+
if (!key?.startsWith(KEY_PREFIX))
|
|
83
|
+
continue;
|
|
84
|
+
const raw = localStorage.getItem(key);
|
|
85
|
+
if (!raw)
|
|
86
|
+
continue;
|
|
87
|
+
try {
|
|
88
|
+
const data = JSON.parse(raw);
|
|
89
|
+
if (Date.now() > data.expiresAt) {
|
|
90
|
+
keysToRemove.push(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
keysToRemove.push(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const key of keysToRemove) {
|
|
98
|
+
localStorage.removeItem(key);
|
|
99
|
+
}
|
|
100
|
+
if (keysToRemove.length > 0) {
|
|
101
|
+
logger.debug(`${LOG_PREFIX} Cleaned up ${keysToRemove.length} expired entries`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
getKey(assistantId) {
|
|
109
|
+
return `${KEY_PREFIX}${assistantId}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DocumentProvider, WindowProvider } from '../services/page-interaction/page-interaction-handler';
|
|
2
|
+
type StateChangeCallback = () => void;
|
|
3
|
+
type DocumentProviderCallback = (getDoc: DocumentProvider, getWin: WindowProvider) => void;
|
|
4
|
+
export declare class NavigationManager {
|
|
5
|
+
private _blockedUrl;
|
|
6
|
+
private readonly iframeNavManager;
|
|
7
|
+
private readonly onStateChanged;
|
|
8
|
+
private readonly onDocumentProviderChanged;
|
|
9
|
+
private readonly getPageHost;
|
|
10
|
+
get blockedUrl(): string | null;
|
|
11
|
+
constructor(onStateChanged: StateChangeCallback, onDocumentProviderChanged: DocumentProviderCallback, getPageHost: () => string | null);
|
|
12
|
+
private resolveTargetForUrl;
|
|
13
|
+
checkForReconnect(): boolean;
|
|
14
|
+
openUrl(url: string): void;
|
|
15
|
+
dismissBlockedUrl(): void;
|
|
16
|
+
openBlockedUrl(): void;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
private navigateSameTab;
|
|
19
|
+
private navigateViaIframeWithFallback;
|
|
20
|
+
private openInNewTab;
|
|
21
|
+
private showBlockedUrl;
|
|
22
|
+
private saveStateForReconnect;
|
|
23
|
+
}
|
|
24
|
+
export {};
|