@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,154 @@
|
|
|
1
|
+
import { logger } from '../utils/logger';
|
|
2
|
+
import { resolveToolCallState } from '../utils/tool-status';
|
|
3
|
+
import { GreetingGate } from './greeting-gate';
|
|
4
|
+
export class TextModeManager {
|
|
5
|
+
isStreaming = false;
|
|
6
|
+
streamContent = '';
|
|
7
|
+
agentStatus = null;
|
|
8
|
+
isSessionEnding = false;
|
|
9
|
+
isDelegationActive = false;
|
|
10
|
+
language;
|
|
11
|
+
connectionManager = null;
|
|
12
|
+
chatManager = null;
|
|
13
|
+
persistence = null;
|
|
14
|
+
logPrefix = '';
|
|
15
|
+
currentStreamMessageId = null;
|
|
16
|
+
pendingStreamingUpdate = false;
|
|
17
|
+
apiClient;
|
|
18
|
+
onStateChanged;
|
|
19
|
+
pendingExternalExecutor = null;
|
|
20
|
+
greetingGate = new GreetingGate((text) => { void this.sendMessage(text); });
|
|
21
|
+
constructor(apiClient, onStateChanged) {
|
|
22
|
+
this.apiClient = apiClient;
|
|
23
|
+
this.onStateChanged = onStateChanged;
|
|
24
|
+
}
|
|
25
|
+
setLanguage(language) {
|
|
26
|
+
this.language = language;
|
|
27
|
+
}
|
|
28
|
+
setWaitForGreeting(waitForGreeting) {
|
|
29
|
+
this.greetingGate.waitForGreeting = waitForGreeting;
|
|
30
|
+
}
|
|
31
|
+
async ensureConnection() {
|
|
32
|
+
if (this.connectionManager)
|
|
33
|
+
return this.connectionManager;
|
|
34
|
+
const { TextConnectionManager: TextConnMgr } = await import('./text-connection-manager');
|
|
35
|
+
this.connectionManager = new TextConnMgr(this.apiClient);
|
|
36
|
+
if (this.pendingExternalExecutor) {
|
|
37
|
+
this.connectionManager.setExternalExecutor(this.pendingExternalExecutor);
|
|
38
|
+
}
|
|
39
|
+
return this.connectionManager;
|
|
40
|
+
}
|
|
41
|
+
setupEventHandlers(deps) {
|
|
42
|
+
if (!this.connectionManager)
|
|
43
|
+
return;
|
|
44
|
+
this.connectionManager.removeAllListeners();
|
|
45
|
+
this.chatManager = deps.chatManager;
|
|
46
|
+
this.persistence = deps.persistence;
|
|
47
|
+
this.logPrefix = deps.logPrefix;
|
|
48
|
+
this.connectionManager.on('connectionStateChanged', () => this.onStateChanged());
|
|
49
|
+
this.connectionManager.on('streamDelta', ({ delta, messageId }) => {
|
|
50
|
+
if (!this.isDelegationActive) {
|
|
51
|
+
this.agentStatus = null;
|
|
52
|
+
}
|
|
53
|
+
this.handleStreamDelta(delta, messageId);
|
|
54
|
+
this.scheduleStreamingUpdate();
|
|
55
|
+
});
|
|
56
|
+
this.connectionManager.on('streamCompleted', ({ fullText }) => {
|
|
57
|
+
deps.chatManager.addAssistantMessage(fullText);
|
|
58
|
+
this.resetStreamState();
|
|
59
|
+
this.onStateChanged();
|
|
60
|
+
this.greetingGate.release();
|
|
61
|
+
});
|
|
62
|
+
this.connectionManager.on('toolCall', ({ toolName, status }) => {
|
|
63
|
+
logger.debug(`${deps.logPrefix} Text tool call:`, toolName, status);
|
|
64
|
+
const state = resolveToolCallState(toolName, status, this.isDelegationActive, this.language);
|
|
65
|
+
this.agentStatus = state.agentStatus;
|
|
66
|
+
this.isDelegationActive = state.isDelegationActive;
|
|
67
|
+
this.onStateChanged();
|
|
68
|
+
});
|
|
69
|
+
this.connectionManager.on('error', (err) => {
|
|
70
|
+
logger.error(`${deps.logPrefix} Text connection error:`, err);
|
|
71
|
+
this.isStreaming = false;
|
|
72
|
+
deps.onError();
|
|
73
|
+
this.onStateChanged();
|
|
74
|
+
});
|
|
75
|
+
this.connectionManager.on('stopConversation', deps.onStopConversation);
|
|
76
|
+
this.connectionManager.on('openUrl', (url) => deps.navigationManager.openUrl(url));
|
|
77
|
+
this.connectionManager.on('openFile', (url) => deps.navigationManager.openUrl(url));
|
|
78
|
+
}
|
|
79
|
+
async sendMessage(text, pageUrl, pageTitle) {
|
|
80
|
+
if (!this.connectionManager || !this.chatManager || !this.persistence)
|
|
81
|
+
return;
|
|
82
|
+
// Hold the first user message fully (display + send) until the greeting has streamed.
|
|
83
|
+
if (this.greetingGate.shouldHold()) {
|
|
84
|
+
this.greetingGate.enqueue(text);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.chatManager.addUserMessage(text);
|
|
88
|
+
this.isStreaming = true;
|
|
89
|
+
this.onStateChanged();
|
|
90
|
+
try {
|
|
91
|
+
await this.connectionManager.sendMessage(text, pageUrl, pageTitle);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
logger.error(`${this.logPrefix} Failed to send text message:`, err);
|
|
95
|
+
this.isStreaming = false;
|
|
96
|
+
this.onStateChanged();
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
requestGreeting(pageUrl, pageTitle) {
|
|
101
|
+
if (!this.connectionManager)
|
|
102
|
+
return;
|
|
103
|
+
this.greetingGate.beginGreeting();
|
|
104
|
+
this.isStreaming = true;
|
|
105
|
+
this.onStateChanged();
|
|
106
|
+
this.connectionManager.sendGreeting(pageUrl, pageTitle).catch((err) => {
|
|
107
|
+
logger.debug(`${this.logPrefix} Failed to request greeting:`, err);
|
|
108
|
+
this.isStreaming = false;
|
|
109
|
+
this.onStateChanged();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async disconnect() {
|
|
113
|
+
this.greetingGate.reset();
|
|
114
|
+
await this.connectionManager?.disconnect();
|
|
115
|
+
this.resetStreamState();
|
|
116
|
+
}
|
|
117
|
+
getConnectionState() {
|
|
118
|
+
return this.connectionManager?.getConnectionState() ?? 'disconnected';
|
|
119
|
+
}
|
|
120
|
+
setDocumentProvider(getDoc, getWin) {
|
|
121
|
+
this.connectionManager?.setDocumentProvider(getDoc, getWin);
|
|
122
|
+
}
|
|
123
|
+
setExternalExecutor(executor) {
|
|
124
|
+
this.pendingExternalExecutor = executor;
|
|
125
|
+
this.connectionManager?.setExternalExecutor(executor);
|
|
126
|
+
}
|
|
127
|
+
resetStreamState() {
|
|
128
|
+
this.isStreaming = false;
|
|
129
|
+
this.currentStreamMessageId = null;
|
|
130
|
+
this.streamContent = '';
|
|
131
|
+
this.agentStatus = null;
|
|
132
|
+
this.isDelegationActive = false;
|
|
133
|
+
}
|
|
134
|
+
dispose() {
|
|
135
|
+
this.connectionManager?.dispose();
|
|
136
|
+
}
|
|
137
|
+
handleStreamDelta(delta, messageId) {
|
|
138
|
+
if (this.currentStreamMessageId !== messageId) {
|
|
139
|
+
this.currentStreamMessageId = messageId;
|
|
140
|
+
this.streamContent = '';
|
|
141
|
+
this.isStreaming = true;
|
|
142
|
+
}
|
|
143
|
+
this.streamContent += delta;
|
|
144
|
+
}
|
|
145
|
+
scheduleStreamingUpdate() {
|
|
146
|
+
if (this.pendingStreamingUpdate)
|
|
147
|
+
return;
|
|
148
|
+
this.pendingStreamingUpdate = true;
|
|
149
|
+
requestAnimationFrame(() => {
|
|
150
|
+
this.pendingStreamingUpdate = false;
|
|
151
|
+
this.onStateChanged();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChatMessage, ConnectionState, WidgetConfig } from '../types';
|
|
2
|
+
import type { ResolvedTheme } from '../utils/theme-detection';
|
|
3
|
+
export interface TextWidgetState {
|
|
4
|
+
config: WidgetConfig | null;
|
|
5
|
+
resolvedTheme: ResolvedTheme;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
initFailed: boolean;
|
|
8
|
+
hasError: boolean;
|
|
9
|
+
isMinimized: boolean;
|
|
10
|
+
isConnecting: boolean;
|
|
11
|
+
connectionState: ConnectionState;
|
|
12
|
+
isStreaming: boolean;
|
|
13
|
+
streamingContent: string;
|
|
14
|
+
agentStatus: string | null;
|
|
15
|
+
messages: ChatMessage[];
|
|
16
|
+
showTermsModal: boolean;
|
|
17
|
+
hasConsented: boolean;
|
|
18
|
+
blockedUrl: string | null;
|
|
19
|
+
showPopover: boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EventEmitter } from './event-emitter';
|
|
2
|
+
import type { WidgetControllerOptions } from './types';
|
|
3
|
+
import type { ConnectOptions } from '../types';
|
|
4
|
+
import type { TextWidgetState } from './text-types';
|
|
5
|
+
interface TextWidgetControllerEvents {
|
|
6
|
+
stateChanged: (state: TextWidgetState) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare class TextWidgetController extends EventEmitter<TextWidgetControllerEvents> {
|
|
9
|
+
private readonly core;
|
|
10
|
+
private readonly connectionManager;
|
|
11
|
+
private readonly sessionBeacon;
|
|
12
|
+
private isStreaming;
|
|
13
|
+
private currentStreamContent;
|
|
14
|
+
private currentStreamMessageId;
|
|
15
|
+
private agentStatus;
|
|
16
|
+
private isDelegationActive;
|
|
17
|
+
private sessionEnded;
|
|
18
|
+
private greetingOverride;
|
|
19
|
+
private readonly greetingGate;
|
|
20
|
+
constructor(options: WidgetControllerOptions);
|
|
21
|
+
initialize(): Promise<void>;
|
|
22
|
+
connect(options?: ConnectOptions): Promise<void>;
|
|
23
|
+
disconnect(trimFarewell?: boolean): Promise<void>;
|
|
24
|
+
private handleSessionResumed;
|
|
25
|
+
resetConversation(): Promise<void>;
|
|
26
|
+
submitFeedback(messageId: string, rating: number | undefined): void;
|
|
27
|
+
sendMessage(text: string): Promise<void>;
|
|
28
|
+
toggleMinimized(): void;
|
|
29
|
+
setMinimized(minimized: boolean): void;
|
|
30
|
+
acceptTerms(): void;
|
|
31
|
+
declineTerms(): void;
|
|
32
|
+
dismissBlockedUrl(): void;
|
|
33
|
+
openBlockedUrl(): void;
|
|
34
|
+
dismissPopover(): void;
|
|
35
|
+
getState(): TextWidgetState;
|
|
36
|
+
dispose(): void;
|
|
37
|
+
private get language();
|
|
38
|
+
private notifyStateChanged;
|
|
39
|
+
private resetStreamState;
|
|
40
|
+
private setupConnectionHandlers;
|
|
41
|
+
private handleStopConversation;
|
|
42
|
+
private requestGreeting;
|
|
43
|
+
private sendDemoResponse;
|
|
44
|
+
private updateDocumentProviders;
|
|
45
|
+
private setupExtensionBridge;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { isDemoMode } from '../constants/defaults';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
import { resolveToolCallState } from '../utils/tool-status';
|
|
4
|
+
import { initExtensionBridge, refreshExtensionContext } from '../services/page-interaction/extension-bridge';
|
|
5
|
+
import { TextConnectionManager } from './text-connection-manager';
|
|
6
|
+
import { EventEmitter } from './event-emitter';
|
|
7
|
+
import { SessionBeacon } from './session-beacon';
|
|
8
|
+
import { WidgetCore } from './widget-core';
|
|
9
|
+
import { GreetingGate } from './greeting-gate';
|
|
10
|
+
const LOG_PREFIX = '[TextWidgetController]';
|
|
11
|
+
const DEMO_MESSAGE_DELAY_MS = 1000;
|
|
12
|
+
export class TextWidgetController extends EventEmitter {
|
|
13
|
+
core;
|
|
14
|
+
connectionManager;
|
|
15
|
+
sessionBeacon;
|
|
16
|
+
isStreaming = false;
|
|
17
|
+
currentStreamContent = '';
|
|
18
|
+
currentStreamMessageId = null;
|
|
19
|
+
agentStatus = null;
|
|
20
|
+
isDelegationActive = false;
|
|
21
|
+
sessionEnded = false;
|
|
22
|
+
greetingOverride;
|
|
23
|
+
greetingGate = new GreetingGate((text) => { void this.sendMessage(text); });
|
|
24
|
+
constructor(options) {
|
|
25
|
+
super();
|
|
26
|
+
const notify = () => this.notifyStateChanged();
|
|
27
|
+
this.core = new WidgetCore(options, notify, (getDoc, getWin) => this.updateDocumentProviders(getDoc, getWin), () => this.connect(), LOG_PREFIX);
|
|
28
|
+
this.connectionManager = new TextConnectionManager(this.core.apiClient);
|
|
29
|
+
this.sessionBeacon = new SessionBeacon();
|
|
30
|
+
this.setupConnectionHandlers();
|
|
31
|
+
this.setupExtensionBridge();
|
|
32
|
+
}
|
|
33
|
+
async initialize() {
|
|
34
|
+
await this.core.initialize();
|
|
35
|
+
}
|
|
36
|
+
async connect(options) {
|
|
37
|
+
this.greetingGate.waitForGreeting = options?.waitForGreeting ?? true;
|
|
38
|
+
this.greetingOverride = options?.greeting;
|
|
39
|
+
const config = this.core.configManager.getConfig();
|
|
40
|
+
if (!config)
|
|
41
|
+
return;
|
|
42
|
+
if (!this.core.requireConsentOrConnect(config))
|
|
43
|
+
return;
|
|
44
|
+
const startFresh = this.sessionEnded;
|
|
45
|
+
this.sessionEnded = false;
|
|
46
|
+
this.core.isConnecting = true;
|
|
47
|
+
this.core.hasError = false;
|
|
48
|
+
this.core.chatManager.clearMessages();
|
|
49
|
+
this.notifyStateChanged();
|
|
50
|
+
try {
|
|
51
|
+
if (!startFresh) {
|
|
52
|
+
await this.core.persistence.loadHistory(this.core.chatManager);
|
|
53
|
+
}
|
|
54
|
+
await this.connectionManager.connect();
|
|
55
|
+
this.core.persistence.setAssistantId(config.assistantId);
|
|
56
|
+
const clientId = this.core.apiClient.getClientId();
|
|
57
|
+
if (clientId) {
|
|
58
|
+
this.sessionBeacon.register(this.core.apiClient.getBaseUrl(), clientId, config.assistantId, () => this.handleSessionResumed());
|
|
59
|
+
}
|
|
60
|
+
this.core.persistence.startNewSession();
|
|
61
|
+
if (isDemoMode) {
|
|
62
|
+
this.core.showDemoWelcome(config);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const greetingOn = this.greetingOverride ?? config.greetingEnabled !== false;
|
|
66
|
+
if (greetingOn && this.core.chatManager.getMessages().length === 0) {
|
|
67
|
+
this.requestGreeting();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.debug(`${LOG_PREFIX} Failed to connect:`, err);
|
|
72
|
+
this.core.isConnecting = false;
|
|
73
|
+
this.notifyStateChanged();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async disconnect(trimFarewell = false) {
|
|
77
|
+
this.greetingGate.reset();
|
|
78
|
+
this.sessionBeacon.unregister();
|
|
79
|
+
this.core.persistence.saveToLocalStorage(this.core.chatManager);
|
|
80
|
+
this.core.persistence.endSession(trimFarewell);
|
|
81
|
+
await this.connectionManager.disconnect();
|
|
82
|
+
this.resetStreamState();
|
|
83
|
+
this.notifyStateChanged();
|
|
84
|
+
}
|
|
85
|
+
handleSessionResumed() {
|
|
86
|
+
this.core.persistence.startNewSession();
|
|
87
|
+
}
|
|
88
|
+
async resetConversation() {
|
|
89
|
+
logger.debug(`${LOG_PREFIX} Resetting conversation...`);
|
|
90
|
+
await this.connectionManager.disconnect();
|
|
91
|
+
this.core.chatManager.clearMessages();
|
|
92
|
+
this.resetStreamState();
|
|
93
|
+
await this.core.persistence.resetMemory();
|
|
94
|
+
this.notifyStateChanged();
|
|
95
|
+
await this.connect();
|
|
96
|
+
}
|
|
97
|
+
submitFeedback(messageId, rating) {
|
|
98
|
+
if (rating == null) {
|
|
99
|
+
this.core.chatManager.setFeedback(messageId, undefined);
|
|
100
|
+
this.notifyStateChanged();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.core.chatManager.setFeedback(messageId, rating);
|
|
104
|
+
this.notifyStateChanged();
|
|
105
|
+
this.core.apiClient.submitFeedback(messageId, rating).catch((err) => {
|
|
106
|
+
logger.debug(`${LOG_PREFIX} Failed to submit feedback:`, err);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async sendMessage(text) {
|
|
110
|
+
if (!text.trim())
|
|
111
|
+
return;
|
|
112
|
+
// Hold the first user message fully (display + send) until the greeting has streamed.
|
|
113
|
+
if (this.greetingGate.shouldHold()) {
|
|
114
|
+
this.greetingGate.enqueue(text);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.core.chatManager.addUserMessage(text);
|
|
118
|
+
if (isDemoMode) {
|
|
119
|
+
this.sendDemoResponse(text);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.isStreaming = true;
|
|
123
|
+
this.notifyStateChanged();
|
|
124
|
+
const refreshedContext = await refreshExtensionContext();
|
|
125
|
+
if (refreshedContext) {
|
|
126
|
+
this.core.pageUrl = refreshedContext.url;
|
|
127
|
+
this.core.pageTitle = refreshedContext.title;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await this.connectionManager.sendMessage(text, this.core.pageUrl, this.core.pageTitle);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logger.error(`${LOG_PREFIX} Failed to send message:`, err);
|
|
134
|
+
this.isStreaming = false;
|
|
135
|
+
this.core.hasError = true;
|
|
136
|
+
this.notifyStateChanged();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
toggleMinimized() {
|
|
140
|
+
this.core.toggleMinimized();
|
|
141
|
+
}
|
|
142
|
+
setMinimized(minimized) {
|
|
143
|
+
this.core.setMinimized(minimized);
|
|
144
|
+
}
|
|
145
|
+
acceptTerms() {
|
|
146
|
+
this.core.acceptTerms(() => this.connect());
|
|
147
|
+
}
|
|
148
|
+
declineTerms() {
|
|
149
|
+
this.core.declineTerms();
|
|
150
|
+
}
|
|
151
|
+
dismissBlockedUrl() {
|
|
152
|
+
this.core.dismissBlockedUrl();
|
|
153
|
+
}
|
|
154
|
+
openBlockedUrl() {
|
|
155
|
+
this.core.openBlockedUrl();
|
|
156
|
+
}
|
|
157
|
+
dismissPopover() {
|
|
158
|
+
this.core.dismissPopover();
|
|
159
|
+
}
|
|
160
|
+
getState() {
|
|
161
|
+
return {
|
|
162
|
+
config: this.core.configManager.getConfig(),
|
|
163
|
+
resolvedTheme: this.core.themeManager.resolvedTheme,
|
|
164
|
+
isLoading: this.core.isLoading,
|
|
165
|
+
initFailed: this.core.initFailed,
|
|
166
|
+
hasError: this.core.hasError,
|
|
167
|
+
isMinimized: this.core.isMinimized,
|
|
168
|
+
isConnecting: this.core.isConnecting,
|
|
169
|
+
connectionState: this.connectionManager.getConnectionState(),
|
|
170
|
+
isStreaming: this.isStreaming,
|
|
171
|
+
streamingContent: this.currentStreamContent,
|
|
172
|
+
agentStatus: this.agentStatus,
|
|
173
|
+
messages: this.core.chatManager.getMessages(),
|
|
174
|
+
showTermsModal: this.core.consentManager.showTermsModal,
|
|
175
|
+
hasConsented: this.core.consentManager.hasConsented,
|
|
176
|
+
blockedUrl: this.core.navigationManager.blockedUrl,
|
|
177
|
+
showPopover: this.core.popoverManager.showPopover,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
dispose() {
|
|
181
|
+
this.connectionManager.dispose();
|
|
182
|
+
this.core.dispose();
|
|
183
|
+
this.removeAllListeners();
|
|
184
|
+
}
|
|
185
|
+
get language() {
|
|
186
|
+
return this.core.configManager.getConfig()?.language;
|
|
187
|
+
}
|
|
188
|
+
notifyStateChanged() {
|
|
189
|
+
this.emit('stateChanged', this.getState());
|
|
190
|
+
}
|
|
191
|
+
resetStreamState() {
|
|
192
|
+
this.isStreaming = false;
|
|
193
|
+
this.currentStreamMessageId = null;
|
|
194
|
+
this.currentStreamContent = '';
|
|
195
|
+
this.agentStatus = null;
|
|
196
|
+
this.isDelegationActive = false;
|
|
197
|
+
}
|
|
198
|
+
setupConnectionHandlers() {
|
|
199
|
+
this.connectionManager.on('connectionStateChanged', (state) => {
|
|
200
|
+
if (state !== 'connecting') {
|
|
201
|
+
this.core.isConnecting = false;
|
|
202
|
+
}
|
|
203
|
+
this.notifyStateChanged();
|
|
204
|
+
});
|
|
205
|
+
this.connectionManager.on('streamDelta', ({ delta, messageId }) => {
|
|
206
|
+
if (this.currentStreamMessageId !== messageId) {
|
|
207
|
+
this.currentStreamMessageId = messageId;
|
|
208
|
+
this.currentStreamContent = '';
|
|
209
|
+
this.isStreaming = true;
|
|
210
|
+
}
|
|
211
|
+
if (!this.isDelegationActive) {
|
|
212
|
+
this.agentStatus = null;
|
|
213
|
+
}
|
|
214
|
+
this.currentStreamContent += delta;
|
|
215
|
+
this.notifyStateChanged();
|
|
216
|
+
});
|
|
217
|
+
this.connectionManager.on('streamCompleted', ({ fullText }) => {
|
|
218
|
+
this.core.chatManager.addAssistantMessage(fullText);
|
|
219
|
+
this.resetStreamState();
|
|
220
|
+
this.notifyStateChanged();
|
|
221
|
+
// The greeting finished — replay any user message held meanwhile (after the greeting).
|
|
222
|
+
this.greetingGate.release();
|
|
223
|
+
});
|
|
224
|
+
this.connectionManager.on('toolCall', ({ toolName, status, agentLabel }) => {
|
|
225
|
+
logger.debug(`${LOG_PREFIX} Tool call:`, toolName, status, agentLabel);
|
|
226
|
+
const state = resolveToolCallState(toolName, status, this.isDelegationActive, this.language, agentLabel);
|
|
227
|
+
this.agentStatus = state.agentStatus;
|
|
228
|
+
this.isDelegationActive = state.isDelegationActive;
|
|
229
|
+
this.notifyStateChanged();
|
|
230
|
+
});
|
|
231
|
+
this.connectionManager.on('error', (err) => {
|
|
232
|
+
logger.error(`${LOG_PREFIX} Connection error:`, err);
|
|
233
|
+
this.core.hasError = true;
|
|
234
|
+
this.isStreaming = false;
|
|
235
|
+
this.notifyStateChanged();
|
|
236
|
+
});
|
|
237
|
+
this.connectionManager.on('stopConversation', () => this.handleStopConversation());
|
|
238
|
+
this.connectionManager.on('openUrl', (url) => this.core.navigationManager.openUrl(url));
|
|
239
|
+
this.connectionManager.on('openFile', (url) => this.core.navigationManager.openUrl(url));
|
|
240
|
+
}
|
|
241
|
+
async handleStopConversation() {
|
|
242
|
+
logger.debug(`${LOG_PREFIX} StopConversation received`);
|
|
243
|
+
this.sessionEnded = true;
|
|
244
|
+
await this.disconnect(true);
|
|
245
|
+
this.core.isMinimized = true;
|
|
246
|
+
this.notifyStateChanged();
|
|
247
|
+
}
|
|
248
|
+
requestGreeting() {
|
|
249
|
+
this.greetingGate.beginGreeting();
|
|
250
|
+
this.isStreaming = true;
|
|
251
|
+
this.notifyStateChanged();
|
|
252
|
+
this.connectionManager.sendGreeting(this.core.pageUrl, this.core.pageTitle).catch((err) => {
|
|
253
|
+
logger.debug(`${LOG_PREFIX} Failed to request greeting:`, err);
|
|
254
|
+
this.isStreaming = false;
|
|
255
|
+
this.notifyStateChanged();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
sendDemoResponse(text) {
|
|
259
|
+
this.isStreaming = true;
|
|
260
|
+
this.notifyStateChanged();
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
this.core.chatManager.addAssistantMessage(`Dies ist eine Demo-Antwort auf: "${text}"\n\nIm Produktivbetrieb würde hier die KI-Antwort erscheinen.`);
|
|
263
|
+
this.isStreaming = false;
|
|
264
|
+
this.notifyStateChanged();
|
|
265
|
+
}, DEMO_MESSAGE_DELAY_MS);
|
|
266
|
+
}
|
|
267
|
+
updateDocumentProviders(getDoc, getWin) {
|
|
268
|
+
this.connectionManager.setDocumentProvider(getDoc, getWin);
|
|
269
|
+
}
|
|
270
|
+
setupExtensionBridge() {
|
|
271
|
+
initExtensionBridge([
|
|
272
|
+
(executor) => this.connectionManager.setExternalExecutor(executor),
|
|
273
|
+
], (ctx) => {
|
|
274
|
+
this.core.pageUrl = ctx.url;
|
|
275
|
+
this.core.pageTitle = ctx.title;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ResolvedTheme } from '../utils/theme-detection';
|
|
2
|
+
type ThemeChangeCallback = (theme: ResolvedTheme) => void;
|
|
3
|
+
export declare class ThemeManager {
|
|
4
|
+
private _resolvedTheme;
|
|
5
|
+
private observerCleanup;
|
|
6
|
+
get resolvedTheme(): ResolvedTheme;
|
|
7
|
+
initialize(configTheme: string, onChange: ThemeChangeCallback): void;
|
|
8
|
+
dispose(): void;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { detectHostTheme, observeThemeChanges } from '../utils/theme-detection';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
export class ThemeManager {
|
|
4
|
+
_resolvedTheme = 'dark';
|
|
5
|
+
observerCleanup = null;
|
|
6
|
+
get resolvedTheme() {
|
|
7
|
+
return this._resolvedTheme;
|
|
8
|
+
}
|
|
9
|
+
initialize(configTheme, onChange) {
|
|
10
|
+
if (configTheme !== 'auto') {
|
|
11
|
+
this._resolvedTheme = configTheme === 'light' ? 'light' : 'dark';
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
this._resolvedTheme = detectHostTheme();
|
|
15
|
+
logger.debug('[ThemeManager] Auto theme detected:', this._resolvedTheme);
|
|
16
|
+
this.observerCleanup = observeThemeChanges((newTheme) => {
|
|
17
|
+
this._resolvedTheme = newTheme;
|
|
18
|
+
onChange(newTheme);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
dispose() {
|
|
22
|
+
this.observerCleanup?.();
|
|
23
|
+
this.observerCleanup = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ChatMessage, ConnectionState, WidgetConfig } from '../types';
|
|
2
|
+
import type { ResolvedTheme } from '../utils/theme-detection';
|
|
3
|
+
import type { WidgetApiClient, WidgetTokenProvider } from '../services/api';
|
|
4
|
+
export type ChatMode = 'voice' | 'text';
|
|
5
|
+
export interface WidgetState {
|
|
6
|
+
config: WidgetConfig | null;
|
|
7
|
+
resolvedTheme: ResolvedTheme;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
initFailed: boolean;
|
|
10
|
+
hasError: boolean;
|
|
11
|
+
isMicrophoneBlocked: boolean;
|
|
12
|
+
isMinimized: boolean;
|
|
13
|
+
isConnecting: boolean;
|
|
14
|
+
connectionState: ConnectionState;
|
|
15
|
+
currentMode: ChatMode;
|
|
16
|
+
isMuted: boolean;
|
|
17
|
+
isMicrophoneReady: boolean;
|
|
18
|
+
isUserSpeaking: boolean;
|
|
19
|
+
/** Normalised microphone input level (0..1), updated continuously during voice. */
|
|
20
|
+
audioLevel: number;
|
|
21
|
+
isAiThinking: boolean;
|
|
22
|
+
isStreaming: boolean;
|
|
23
|
+
/** Assistant audio is still playing back, even after the transcript is complete (voice only). */
|
|
24
|
+
isAssistantSpeaking: boolean;
|
|
25
|
+
streamingContent: string;
|
|
26
|
+
isTranscribing: boolean;
|
|
27
|
+
transcribingContent: string;
|
|
28
|
+
messages: ChatMessage[];
|
|
29
|
+
agentStatus: string | null;
|
|
30
|
+
showTermsModal: boolean;
|
|
31
|
+
hasConsented: boolean;
|
|
32
|
+
blockedUrl: string | null;
|
|
33
|
+
showPopover: boolean;
|
|
34
|
+
}
|
|
35
|
+
export type WidgetEventType = 'stateChanged' | 'error' | 'configLoaded' | 'connected' | 'disconnected' | 'messageReceived';
|
|
36
|
+
export interface WidgetEvents {
|
|
37
|
+
stateChanged: (state: WidgetState) => void;
|
|
38
|
+
error: (error: string) => void;
|
|
39
|
+
configLoaded: (config: WidgetConfig) => void;
|
|
40
|
+
connected: () => void;
|
|
41
|
+
disconnected: () => void;
|
|
42
|
+
messageReceived: (message: ChatMessage) => void;
|
|
43
|
+
}
|
|
44
|
+
export interface WidgetControllerOptions {
|
|
45
|
+
apiKey?: string;
|
|
46
|
+
baseUrl?: string;
|
|
47
|
+
source?: string;
|
|
48
|
+
tokenProvider?: WidgetTokenProvider;
|
|
49
|
+
apiClient?: WidgetApiClient;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { WidgetApiClient } from '../services/api';
|
|
2
|
+
import type { ConnectionManager } from './connection-manager';
|
|
3
|
+
import type { ChatManager } from './chat-manager';
|
|
4
|
+
import type { ConversationPersistence } from './conversation-persistence';
|
|
5
|
+
import type { NavigationManager } from './navigation-manager';
|
|
6
|
+
type StateChangeCallback = () => void;
|
|
7
|
+
type VoiceEventDeps = {
|
|
8
|
+
connectionManager: ConnectionManager;
|
|
9
|
+
apiClient: WidgetApiClient;
|
|
10
|
+
chatManager: ChatManager;
|
|
11
|
+
persistence: ConversationPersistence;
|
|
12
|
+
navigationManager: NavigationManager;
|
|
13
|
+
onStopConversation: () => Promise<void>;
|
|
14
|
+
logPrefix: string;
|
|
15
|
+
};
|
|
16
|
+
export declare class VoiceStateManager {
|
|
17
|
+
isUserSpeaking: boolean;
|
|
18
|
+
isAiThinking: boolean;
|
|
19
|
+
isMuted: boolean;
|
|
20
|
+
isMicrophoneReady: boolean;
|
|
21
|
+
isMicrophoneBlocked: boolean;
|
|
22
|
+
isTranscribing: boolean;
|
|
23
|
+
transcribingContent: string;
|
|
24
|
+
isStreaming: boolean;
|
|
25
|
+
isAssistantSpeaking: boolean;
|
|
26
|
+
streamContent: string;
|
|
27
|
+
agentStatus: string | null;
|
|
28
|
+
audioLevel: number;
|
|
29
|
+
private audioLevelLastTime;
|
|
30
|
+
private isDelegationActive;
|
|
31
|
+
private connectionManager;
|
|
32
|
+
private apiClient;
|
|
33
|
+
private logPrefix;
|
|
34
|
+
language?: string;
|
|
35
|
+
private latestUsage;
|
|
36
|
+
private accumulatedUsage;
|
|
37
|
+
private pendingToolCalls;
|
|
38
|
+
private thinkingStartedAt;
|
|
39
|
+
private pendingStreamingUpdate;
|
|
40
|
+
private toolAbortController;
|
|
41
|
+
private readonly onStateChanged;
|
|
42
|
+
constructor(onStateChanged: StateChangeCallback);
|
|
43
|
+
setupEventHandlers(deps: VoiceEventDeps): void;
|
|
44
|
+
setLanguage(language?: string): void;
|
|
45
|
+
toggleMute(): void;
|
|
46
|
+
/**
|
|
47
|
+
* User pressed stop while the assistant was busy. If a backend tool (e.g. a
|
|
48
|
+
* long web research) is in flight, abort its HTTP call so the server cancels
|
|
49
|
+
* it (RequestAborted) and let the model wrap up gracefully. Otherwise the
|
|
50
|
+
* assistant is thinking/talking — interrupt the response and drop the audio.
|
|
51
|
+
*/
|
|
52
|
+
stop(): void;
|
|
53
|
+
/**
|
|
54
|
+
* The assistant transcript completes before its audio finishes playing.
|
|
55
|
+
* Hold the speaking flag until playback truly ends (audioDone + playback
|
|
56
|
+
* monitor) so the stop button stays visible while the user can still hear
|
|
57
|
+
* the assistant.
|
|
58
|
+
*/
|
|
59
|
+
private trackAssistantPlayback;
|
|
60
|
+
resetState(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Asymmetric envelope follower over a dBFS-mapped input. Two stages:
|
|
63
|
+
*
|
|
64
|
+
* 1. dBFS perceptual map — raw RMS (0..1) lives mostly under 0.1 so a
|
|
65
|
+
* linear halo would barely move at normal volumes. Mapping through
|
|
66
|
+
* 20·log10(level) and clamping to roughly -50..0 dBFS spreads the
|
|
67
|
+
* normal-speech range across the visual.
|
|
68
|
+
* 2. One-pole IIR with split time constants — 10 ms attack so syllables
|
|
69
|
+
* pop, 150 ms release so the avatar doesn't twitch off between words.
|
|
70
|
+
*
|
|
71
|
+
* Sweet spots picked from common voice-visualisation guidance (attack
|
|
72
|
+
* 5–50 ms, release 100–300 ms, dBFS floor -60 .. -50, ceiling -10 .. 0).
|
|
73
|
+
*/
|
|
74
|
+
private smoothAudioLevel;
|
|
75
|
+
prepareForConnection(): void;
|
|
76
|
+
private setupSpeechHandlers;
|
|
77
|
+
private setupConnectionHandlers;
|
|
78
|
+
private setupActionHandlers;
|
|
79
|
+
private handleToolCall;
|
|
80
|
+
private handleFunctionCall;
|
|
81
|
+
private handleStopConversation;
|
|
82
|
+
private accumulateUsage;
|
|
83
|
+
private scheduleStreamingUpdate;
|
|
84
|
+
}
|
|
85
|
+
export {};
|