@convai/web-sdk 0.1.1-beta.4 → 0.2.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/README.md +735 -189
- package/dist/core/AudioManager.d.ts +8 -0
- package/dist/core/AudioManager.d.ts.map +1 -1
- package/dist/core/AudioManager.js +45 -5
- package/dist/core/AudioManager.js.map +1 -1
- package/dist/core/ConvaiClient.d.ts +22 -9
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +92 -41
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +27 -24
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/ScreenShareManager.d.ts.map +1 -1
- package/dist/core/ScreenShareManager.js +4 -0
- package/dist/core/ScreenShareManager.js.map +1 -1
- package/dist/core/VideoManager.d.ts.map +1 -1
- package/dist/core/VideoManager.js +2 -0
- package/dist/core/VideoManager.js.map +1 -1
- package/dist/core/types.d.ts +13 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.d.ts +3 -0
- package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.js +25 -22
- package/dist/react/components/ConvaiWidget.js.map +1 -1
- package/dist/react/components/index.d.ts +0 -1
- package/dist/react/components/index.d.ts.map +1 -1
- package/dist/react/components/index.js +0 -2
- package/dist/react/components/index.js.map +1 -1
- package/dist/react/hooks/useConvaiClient.d.ts +3 -0
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +46 -9
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/types/index.d.ts +1 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/vanilla/AudioRenderer.d.ts +45 -0
- package/dist/vanilla/AudioRenderer.d.ts.map +1 -0
- package/dist/vanilla/AudioRenderer.js +126 -0
- package/dist/vanilla/AudioRenderer.js.map +1 -0
- package/dist/vanilla/ConvaiWidget.d.ts +39 -0
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -0
- package/dist/vanilla/ConvaiWidget.js +1835 -0
- package/dist/vanilla/ConvaiWidget.js.map +1 -0
- package/dist/vanilla/icons.d.ts +34 -0
- package/dist/vanilla/icons.d.ts.map +1 -0
- package/dist/vanilla/icons.js +215 -0
- package/dist/vanilla/icons.js.map +1 -0
- package/dist/vanilla/index.d.ts +35 -0
- package/dist/vanilla/index.d.ts.map +1 -0
- package/dist/vanilla/index.js +37 -0
- package/dist/vanilla/index.js.map +1 -0
- package/dist/{components/rtc-widget/styles/theme.d.ts → vanilla/styles.d.ts} +14 -50
- package/dist/vanilla/styles.d.ts.map +1 -0
- package/dist/vanilla/styles.js +287 -0
- package/dist/vanilla/styles.js.map +1 -0
- package/dist/vanilla/types.d.ts +38 -0
- package/dist/vanilla/types.d.ts.map +1 -0
- package/dist/vanilla/types.js +2 -0
- package/dist/vanilla/types.js.map +1 -0
- package/package.json +12 -5
- package/dist/components/ConvaiWidget.d.ts +0 -59
- package/dist/components/ConvaiWidget.d.ts.map +0 -1
- package/dist/components/ConvaiWidget.js +0 -421
- package/dist/components/ConvaiWidget.js.map +0 -1
- package/dist/components/index.d.ts +0 -3
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js +0 -5
- package/dist/components/index.js.map +0 -1
- package/dist/components/rtc-widget/components/AudioSettingsPanel.d.ts +0 -10
- package/dist/components/rtc-widget/components/AudioSettingsPanel.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/AudioSettingsPanel.js +0 -316
- package/dist/components/rtc-widget/components/AudioSettingsPanel.js.map +0 -1
- package/dist/components/rtc-widget/components/ConviMessage.d.ts +0 -10
- package/dist/components/rtc-widget/components/ConviMessage.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/ConviMessage.js +0 -14
- package/dist/components/rtc-widget/components/ConviMessage.js.map +0 -1
- package/dist/components/rtc-widget/components/FloatingVideo.d.ts +0 -9
- package/dist/components/rtc-widget/components/FloatingVideo.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/FloatingVideo.js +0 -122
- package/dist/components/rtc-widget/components/FloatingVideo.js.map +0 -1
- package/dist/components/rtc-widget/components/MarkdownRenderer.d.ts +0 -7
- package/dist/components/rtc-widget/components/MarkdownRenderer.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/MarkdownRenderer.js +0 -68
- package/dist/components/rtc-widget/components/MarkdownRenderer.js.map +0 -1
- package/dist/components/rtc-widget/components/MessageBubble.d.ts +0 -10
- package/dist/components/rtc-widget/components/MessageBubble.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/MessageBubble.js +0 -23
- package/dist/components/rtc-widget/components/MessageBubble.js.map +0 -1
- package/dist/components/rtc-widget/components/MessageList.d.ts +0 -11
- package/dist/components/rtc-widget/components/MessageList.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/MessageList.js +0 -89
- package/dist/components/rtc-widget/components/MessageList.js.map +0 -1
- package/dist/components/rtc-widget/components/UserMessage.d.ts +0 -9
- package/dist/components/rtc-widget/components/UserMessage.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/UserMessage.js +0 -15
- package/dist/components/rtc-widget/components/UserMessage.js.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/ConviButton.d.ts +0 -6
- package/dist/components/rtc-widget/components/conviComponents/ConviButton.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/ConviButton.js +0 -15
- package/dist/components/rtc-widget/components/conviComponents/ConviButton.js.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/ConviFooter.d.ts +0 -25
- package/dist/components/rtc-widget/components/conviComponents/ConviFooter.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/ConviFooter.js +0 -172
- package/dist/components/rtc-widget/components/conviComponents/ConviFooter.js.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/ConviHeader.d.ts +0 -17
- package/dist/components/rtc-widget/components/conviComponents/ConviHeader.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/ConviHeader.js +0 -66
- package/dist/components/rtc-widget/components/conviComponents/ConviHeader.js.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +0 -12
- package/dist/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/SettingsTray.js +0 -68
- package/dist/components/rtc-widget/components/conviComponents/SettingsTray.js.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/VoiceModeOverlay.d.ts +0 -8
- package/dist/components/rtc-widget/components/conviComponents/VoiceModeOverlay.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js +0 -199
- package/dist/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/index.d.ts +0 -6
- package/dist/components/rtc-widget/components/conviComponents/index.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/conviComponents/index.js +0 -6
- package/dist/components/rtc-widget/components/conviComponents/index.js.map +0 -1
- package/dist/components/rtc-widget/components/index.d.ts +0 -8
- package/dist/components/rtc-widget/components/index.d.ts.map +0 -1
- package/dist/components/rtc-widget/components/index.js +0 -13
- package/dist/components/rtc-widget/components/index.js.map +0 -1
- package/dist/components/rtc-widget/index.d.ts +0 -6
- package/dist/components/rtc-widget/index.d.ts.map +0 -1
- package/dist/components/rtc-widget/index.js +0 -9
- package/dist/components/rtc-widget/index.js.map +0 -1
- package/dist/components/rtc-widget/styles/framerConfig.d.ts +0 -116
- package/dist/components/rtc-widget/styles/framerConfig.d.ts.map +0 -1
- package/dist/components/rtc-widget/styles/framerConfig.js +0 -73
- package/dist/components/rtc-widget/styles/framerConfig.js.map +0 -1
- package/dist/components/rtc-widget/styles/icons.d.ts +0 -28
- package/dist/components/rtc-widget/styles/icons.d.ts.map +0 -1
- package/dist/components/rtc-widget/styles/icons.js +0 -257
- package/dist/components/rtc-widget/styles/icons.js.map +0 -1
- package/dist/components/rtc-widget/styles/index.d.ts +0 -6
- package/dist/components/rtc-widget/styles/index.d.ts.map +0 -1
- package/dist/components/rtc-widget/styles/index.js +0 -9
- package/dist/components/rtc-widget/styles/index.js.map +0 -1
- package/dist/components/rtc-widget/styles/styledComponents.d.ts +0 -90
- package/dist/components/rtc-widget/styles/styledComponents.d.ts.map +0 -1
- package/dist/components/rtc-widget/styles/styledComponents.js +0 -661
- package/dist/components/rtc-widget/styles/styledComponents.js.map +0 -1
- package/dist/components/rtc-widget/styles/theme.d.ts.map +0 -1
- package/dist/components/rtc-widget/styles/theme.js +0 -290
- package/dist/components/rtc-widget/styles/theme.js.map +0 -1
- package/dist/components/rtc-widget/types/index.d.ts +0 -60
- package/dist/components/rtc-widget/types/index.d.ts.map +0 -1
- package/dist/components/rtc-widget/types/index.js +0 -2
- package/dist/components/rtc-widget/types/index.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -13
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -14
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/useAudioControls.d.ts +0 -41
- package/dist/hooks/useAudioControls.d.ts.map +0 -1
- package/dist/hooks/useAudioControls.js +0 -208
- package/dist/hooks/useAudioControls.js.map +0 -1
- package/dist/hooks/useCharacterInfo.d.ts +0 -17
- package/dist/hooks/useCharacterInfo.d.ts.map +0 -1
- package/dist/hooks/useCharacterInfo.js +0 -60
- package/dist/hooks/useCharacterInfo.js.map +0 -1
- package/dist/hooks/useConvaiClient.d.ts +0 -30
- package/dist/hooks/useConvaiClient.d.ts.map +0 -1
- package/dist/hooks/useConvaiClient.js +0 -349
- package/dist/hooks/useConvaiClient.js.map +0 -1
- package/dist/hooks/useDynamicInfoUpdater.d.ts +0 -33
- package/dist/hooks/useDynamicInfoUpdater.d.ts.map +0 -1
- package/dist/hooks/useDynamicInfoUpdater.js +0 -49
- package/dist/hooks/useDynamicInfoUpdater.js.map +0 -1
- package/dist/hooks/useLocalCameraTrack.d.ts +0 -22
- package/dist/hooks/useLocalCameraTrack.d.ts.map +0 -1
- package/dist/hooks/useLocalCameraTrack.js +0 -34
- package/dist/hooks/useLocalCameraTrack.js.map +0 -1
- package/dist/hooks/useMessageHandler.d.ts +0 -28
- package/dist/hooks/useMessageHandler.d.ts.map +0 -1
- package/dist/hooks/useMessageHandler.js +0 -267
- package/dist/hooks/useMessageHandler.js.map +0 -1
- package/dist/hooks/useScreenShare.d.ts +0 -45
- package/dist/hooks/useScreenShare.d.ts.map +0 -1
- package/dist/hooks/useScreenShare.js +0 -186
- package/dist/hooks/useScreenShare.js.map +0 -1
- package/dist/hooks/useTemplateKeysUpdater.d.ts +0 -35
- package/dist/hooks/useTemplateKeysUpdater.d.ts.map +0 -1
- package/dist/hooks/useTemplateKeysUpdater.js +0 -51
- package/dist/hooks/useTemplateKeysUpdater.js.map +0 -1
- package/dist/hooks/useTriggerMessageSender.d.ts +0 -28
- package/dist/hooks/useTriggerMessageSender.d.ts.map +0 -1
- package/dist/hooks/useTriggerMessageSender.js +0 -46
- package/dist/hooks/useTriggerMessageSender.js.map +0 -1
- package/dist/hooks/useTtsToggle.d.ts +0 -37
- package/dist/hooks/useTtsToggle.d.ts.map +0 -1
- package/dist/hooks/useTtsToggle.js +0 -63
- package/dist/hooks/useTtsToggle.js.map +0 -1
- package/dist/hooks/useUserTextMessageSender.d.ts +0 -28
- package/dist/hooks/useUserTextMessageSender.d.ts.map +0 -1
- package/dist/hooks/useUserTextMessageSender.js +0 -58
- package/dist/hooks/useUserTextMessageSender.js.map +0 -1
- package/dist/hooks/useVideoControls.d.ts +0 -39
- package/dist/hooks/useVideoControls.d.ts.map +0 -1
- package/dist/hooks/useVideoControls.js +0 -193
- package/dist/hooks/useVideoControls.js.map +0 -1
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanilla ConvaiWidget - Complete UI widget for Convai conversations
|
|
3
|
+
* Ports the React ConvaiWidget to vanilla TypeScript with DOM manipulation
|
|
4
|
+
*/
|
|
5
|
+
import { AudioRenderer } from "./AudioRenderer";
|
|
6
|
+
import { aeroTheme, injectGlobalStyles } from "./styles";
|
|
7
|
+
import { Icons } from "./icons";
|
|
8
|
+
/**
|
|
9
|
+
* Create a Convai chat widget in the specified container
|
|
10
|
+
*
|
|
11
|
+
* @param container - HTML element to attach the widget to
|
|
12
|
+
* @param options - Widget configuration options
|
|
13
|
+
* @returns VanillaWidget instance with destroy method
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { ConvaiClient, createConvaiWidget } from '@convai/web-sdk/vanilla';
|
|
18
|
+
*
|
|
19
|
+
* const client = new ConvaiClient();
|
|
20
|
+
* await client.connect({
|
|
21
|
+
* apiKey: 'your-api-key',
|
|
22
|
+
* characterId: 'your-character-id'
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* const widget = createConvaiWidget(document.body, {
|
|
26
|
+
* convaiClient: client,
|
|
27
|
+
* showVideo: true,
|
|
28
|
+
* showScreenShare: true
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // Later, cleanup
|
|
32
|
+
* widget.destroy();
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function createConvaiWidget(container, options) {
|
|
36
|
+
const { convaiClient, showVideo = true, showScreenShare = true } = options;
|
|
37
|
+
// Inject global styles
|
|
38
|
+
injectGlobalStyles();
|
|
39
|
+
// State
|
|
40
|
+
let isOpen = false;
|
|
41
|
+
let isSettingsOpen = false;
|
|
42
|
+
let isVoiceMode = false;
|
|
43
|
+
let isMuted = false;
|
|
44
|
+
let isVideoVisible = false;
|
|
45
|
+
let inputValue = "";
|
|
46
|
+
let characterName = "Character";
|
|
47
|
+
let characterImage = "";
|
|
48
|
+
let audioRenderer = null;
|
|
49
|
+
// DOM elements (will be created below)
|
|
50
|
+
let rootElement;
|
|
51
|
+
let morphingContainer;
|
|
52
|
+
let buttonContent;
|
|
53
|
+
let chatContent;
|
|
54
|
+
let headerElement;
|
|
55
|
+
let contentElement;
|
|
56
|
+
let footerElement;
|
|
57
|
+
let inputElement;
|
|
58
|
+
let messageListElement;
|
|
59
|
+
let settingsTray;
|
|
60
|
+
let floatingVideo;
|
|
61
|
+
let voiceModeOverlay;
|
|
62
|
+
// Audio Analysis State
|
|
63
|
+
let audioContext = null;
|
|
64
|
+
let analyzer = null;
|
|
65
|
+
let dataArray = null;
|
|
66
|
+
let rafId = null;
|
|
67
|
+
let source = null;
|
|
68
|
+
// Fetch character info - matches React useCharacterInfo hook
|
|
69
|
+
const fetchCharacterInfo = async () => {
|
|
70
|
+
if (!convaiClient.apiKey || !convaiClient.characterId)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch("https://api.convai.com/character/get", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
"CONVAI-API-KEY": convaiClient.apiKey,
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ charID: convaiClient.characterId }),
|
|
80
|
+
});
|
|
81
|
+
if (response.ok) {
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
// Extract character name and image with fallbacks - matches React version
|
|
84
|
+
characterName = data.character_name || "Convi";
|
|
85
|
+
characterImage =
|
|
86
|
+
data.model_details?.METAHUMAN?.avatar_image_square ||
|
|
87
|
+
data.model_details?.METAHUMAN?.avatar_image ||
|
|
88
|
+
data.model_details?.modelPlaceholder ||
|
|
89
|
+
"";
|
|
90
|
+
updateHeader();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error("Failed to fetch character info:", error);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Create root structure
|
|
98
|
+
const createDOM = () => {
|
|
99
|
+
rootElement = document.createElement("div");
|
|
100
|
+
rootElement.className = "convai-widget";
|
|
101
|
+
rootElement.style.cssText = `
|
|
102
|
+
position: fixed;
|
|
103
|
+
bottom: 1.5rem;
|
|
104
|
+
right: 1.5rem;
|
|
105
|
+
z-index: ${aeroTheme.zIndex.modal};
|
|
106
|
+
font-family: ${aeroTheme.typography.fontFamily.primary};
|
|
107
|
+
`;
|
|
108
|
+
// Morphing container
|
|
109
|
+
morphingContainer = document.createElement("div");
|
|
110
|
+
morphingContainer.style.cssText = `
|
|
111
|
+
position: relative;
|
|
112
|
+
width: 4rem;
|
|
113
|
+
height: 4rem;
|
|
114
|
+
background: ${aeroTheme.colors.glass.backdrop};
|
|
115
|
+
backdrop-filter: ${aeroTheme.glass.backdrop};
|
|
116
|
+
border: ${aeroTheme.glass.border};
|
|
117
|
+
border-radius: 50%;
|
|
118
|
+
box-shadow: ${aeroTheme.shadows.glass};
|
|
119
|
+
transition: all 0.3s ease-in-out;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
`;
|
|
126
|
+
// Button content (logo)
|
|
127
|
+
buttonContent = document.createElement("div");
|
|
128
|
+
buttonContent.style.cssText = `
|
|
129
|
+
position: absolute;
|
|
130
|
+
inset: 0;
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
transition: opacity 0.3s, transform 0.3s;
|
|
135
|
+
opacity: 1;
|
|
136
|
+
transform: scale(1);
|
|
137
|
+
`;
|
|
138
|
+
const convaiLogo = Icons.ConvaiLogo("xl", "idle");
|
|
139
|
+
convaiLogo.style.color = aeroTheme.colors.convai.light;
|
|
140
|
+
buttonContent.appendChild(convaiLogo);
|
|
141
|
+
// Chat content
|
|
142
|
+
chatContent = document.createElement("div");
|
|
143
|
+
chatContent.style.cssText = `
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset: 0;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
opacity: 0;
|
|
149
|
+
transform: scale(0.8);
|
|
150
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
`;
|
|
153
|
+
// Voice Mode Overlay
|
|
154
|
+
voiceModeOverlay = createVoiceModeOverlay();
|
|
155
|
+
chatContent.appendChild(voiceModeOverlay);
|
|
156
|
+
// Header
|
|
157
|
+
headerElement = createHeader();
|
|
158
|
+
// Content area
|
|
159
|
+
contentElement = document.createElement("div");
|
|
160
|
+
contentElement.style.cssText = `
|
|
161
|
+
flex: 1;
|
|
162
|
+
overflow-y: auto;
|
|
163
|
+
padding: 1rem;
|
|
164
|
+
background: transparent;
|
|
165
|
+
`;
|
|
166
|
+
messageListElement = createMessageList();
|
|
167
|
+
contentElement.appendChild(messageListElement);
|
|
168
|
+
// Footer
|
|
169
|
+
footerElement = createFooter();
|
|
170
|
+
chatContent.appendChild(headerElement);
|
|
171
|
+
chatContent.appendChild(contentElement);
|
|
172
|
+
chatContent.appendChild(footerElement);
|
|
173
|
+
morphingContainer.appendChild(buttonContent);
|
|
174
|
+
morphingContainer.appendChild(chatContent);
|
|
175
|
+
rootElement.appendChild(morphingContainer);
|
|
176
|
+
// Settings tray
|
|
177
|
+
settingsTray = createSettingsTray();
|
|
178
|
+
rootElement.appendChild(settingsTray);
|
|
179
|
+
// Floating video
|
|
180
|
+
floatingVideo = createFloatingVideo();
|
|
181
|
+
container.appendChild(floatingVideo);
|
|
182
|
+
container.appendChild(rootElement);
|
|
183
|
+
// Event listeners
|
|
184
|
+
morphingContainer.addEventListener("click", handleToggle);
|
|
185
|
+
};
|
|
186
|
+
// Create Voice Mode Overlay
|
|
187
|
+
const createVoiceModeOverlay = () => {
|
|
188
|
+
const overlay = document.createElement("div");
|
|
189
|
+
overlay.style.cssText = `
|
|
190
|
+
position: absolute;
|
|
191
|
+
top: 50%;
|
|
192
|
+
left: 50%;
|
|
193
|
+
transform: translate(-50%, -50%);
|
|
194
|
+
text-align: center;
|
|
195
|
+
padding: 1rem;
|
|
196
|
+
z-index: 10;
|
|
197
|
+
pointer-events: auto;
|
|
198
|
+
display: none;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 1.5rem;
|
|
202
|
+
`;
|
|
203
|
+
// Bars Container
|
|
204
|
+
const barsContainer = document.createElement("div");
|
|
205
|
+
barsContainer.id = "voice-bars-container";
|
|
206
|
+
barsContainer.style.cssText = `
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
gap: 2px;
|
|
211
|
+
height: 80px;
|
|
212
|
+
max-width: 300px;
|
|
213
|
+
`;
|
|
214
|
+
// Create 40 bars
|
|
215
|
+
for (let i = 0; i < 40; i++) {
|
|
216
|
+
const bar = document.createElement("div");
|
|
217
|
+
bar.className = "voice-bar";
|
|
218
|
+
bar.style.cssText = `
|
|
219
|
+
width: 3px;
|
|
220
|
+
height: 15px;
|
|
221
|
+
background-color: ${aeroTheme.colors.neutral[400]};
|
|
222
|
+
border-radius: 1.5px;
|
|
223
|
+
transition: height 0.08s ease-out, background-color 0.2s;
|
|
224
|
+
transform-origin: center;
|
|
225
|
+
`;
|
|
226
|
+
barsContainer.appendChild(bar);
|
|
227
|
+
}
|
|
228
|
+
overlay.appendChild(barsContainer);
|
|
229
|
+
// Status Text
|
|
230
|
+
const statusContainer = document.createElement("div");
|
|
231
|
+
const statusTitle = document.createElement("div");
|
|
232
|
+
statusTitle.id = "voice-mode-title";
|
|
233
|
+
statusTitle.style.cssText = `
|
|
234
|
+
font-size: 14px;
|
|
235
|
+
font-weight: 500;
|
|
236
|
+
color: ${aeroTheme.colors.text.primary};
|
|
237
|
+
margin-bottom: 0.5rem;
|
|
238
|
+
`;
|
|
239
|
+
statusTitle.textContent = "Voice Only Mode";
|
|
240
|
+
const statusSubtitle = document.createElement("div");
|
|
241
|
+
statusSubtitle.id = "voice-mode-subtitle";
|
|
242
|
+
statusSubtitle.style.cssText = `
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
color: ${aeroTheme.colors.text.secondary};
|
|
245
|
+
`;
|
|
246
|
+
statusSubtitle.textContent = "Press and hold the microphone to talk";
|
|
247
|
+
statusContainer.appendChild(statusTitle);
|
|
248
|
+
statusContainer.appendChild(statusSubtitle);
|
|
249
|
+
overlay.appendChild(statusContainer);
|
|
250
|
+
return overlay;
|
|
251
|
+
};
|
|
252
|
+
// Audio Analysis State for Voice Mode
|
|
253
|
+
let audioLevels = Array(40).fill(0);
|
|
254
|
+
let targetLevels = Array(40).fill(0.05);
|
|
255
|
+
let currentLevels = Array(40).fill(0.05);
|
|
256
|
+
let startTime = 0;
|
|
257
|
+
// Audio Analysis Logic
|
|
258
|
+
const updateAudioBars = () => {
|
|
259
|
+
if (!voiceModeOverlay)
|
|
260
|
+
return;
|
|
261
|
+
const bars = voiceModeOverlay.querySelectorAll(".voice-bar");
|
|
262
|
+
const isTalking = convaiClient.state.isSpeaking;
|
|
263
|
+
const isListening = !convaiClient.audioControls.isAudioMuted;
|
|
264
|
+
const isAnimating = isListening || isTalking;
|
|
265
|
+
// Update colors based on state
|
|
266
|
+
bars.forEach((bar) => {
|
|
267
|
+
bar.style.backgroundColor = isTalking
|
|
268
|
+
? aeroTheme.colors.convai.light
|
|
269
|
+
: isListening
|
|
270
|
+
? aeroTheme.colors.text.primary
|
|
271
|
+
: aeroTheme.colors.neutral[400];
|
|
272
|
+
});
|
|
273
|
+
// Update Text
|
|
274
|
+
const title = document.getElementById("voice-mode-title");
|
|
275
|
+
const subtitle = document.getElementById("voice-mode-subtitle");
|
|
276
|
+
if (title) {
|
|
277
|
+
title.textContent = isTalking
|
|
278
|
+
? "Character Speaking..."
|
|
279
|
+
: isListening
|
|
280
|
+
? "Listening..."
|
|
281
|
+
: "Voice Only Mode";
|
|
282
|
+
}
|
|
283
|
+
if (subtitle) {
|
|
284
|
+
subtitle.textContent =
|
|
285
|
+
isListening || isTalking
|
|
286
|
+
? "Audio active"
|
|
287
|
+
: "Press and hold the microphone to talk";
|
|
288
|
+
}
|
|
289
|
+
// Animation Logic - Matches React version
|
|
290
|
+
if (isListening && analyzer && dataArray) {
|
|
291
|
+
// Use time domain data (waveform) instead of frequency
|
|
292
|
+
// @ts-ignore - TypeScript strict mode issue with Uint8Array type
|
|
293
|
+
analyzer.getByteTimeDomainData(dataArray);
|
|
294
|
+
// Calculate RMS (Root Mean Square) for volume
|
|
295
|
+
let sum = 0;
|
|
296
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
297
|
+
const normalized = (dataArray[i] - 128) / 128; // Center around 0
|
|
298
|
+
sum += normalized * normalized;
|
|
299
|
+
}
|
|
300
|
+
const rms = Math.sqrt(sum / dataArray.length);
|
|
301
|
+
// Apply some scaling and clamping
|
|
302
|
+
const volume = Math.min(1, rms * 3); // Boost sensitivity
|
|
303
|
+
// Create left-to-right wave effect
|
|
304
|
+
const minHeight = 8;
|
|
305
|
+
const maxHeight = 70;
|
|
306
|
+
bars.forEach((bar, i) => {
|
|
307
|
+
// Progressive wave from left to right
|
|
308
|
+
const position = i / 40; // 0 to 1 from left to right
|
|
309
|
+
const wavePhase = Date.now() / 300 + position * Math.PI * 2;
|
|
310
|
+
const waveVariation = Math.sin(wavePhase) * 0.15 + 0.85; // 0.7 to 1.0
|
|
311
|
+
const level = volume * waveVariation;
|
|
312
|
+
const height = minHeight + level * (maxHeight - minHeight);
|
|
313
|
+
bar.style.height = `${Math.max(minHeight, height)}px`;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else if (isTalking) {
|
|
317
|
+
// Simulate speaking bars with natural speech patterns
|
|
318
|
+
const elapsed = (Date.now() - startTime) / 1000; // seconds
|
|
319
|
+
// Generate new random target levels occasionally (simulating syllables/words)
|
|
320
|
+
if (Math.random() < 0.08) {
|
|
321
|
+
// 8% chance per frame = ~5 times per second
|
|
322
|
+
targetLevels = Array(40)
|
|
323
|
+
.fill(0)
|
|
324
|
+
.map((_, i) => {
|
|
325
|
+
// More variation in the middle bars, less on edges for natural spread
|
|
326
|
+
const position = i / 40;
|
|
327
|
+
const centerWeight = 1 - Math.abs(position - 0.5) * 0.5;
|
|
328
|
+
// Random peaks and valleys like speech patterns
|
|
329
|
+
const randomPeak = 0.2 + Math.random() * 0.7; // 0.2 to 0.9
|
|
330
|
+
// Add some neighbor correlation so bars don't jump independently
|
|
331
|
+
const prevTarget = targetLevels[i] || 0.3;
|
|
332
|
+
const correlation = prevTarget * 0.4 + randomPeak * 0.6;
|
|
333
|
+
return correlation * centerWeight;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Smoothly interpolate current levels toward targets (organic movement)
|
|
337
|
+
currentLevels = currentLevels.map((current, i) => {
|
|
338
|
+
const target = targetLevels[i];
|
|
339
|
+
const speed = 0.2; // Smooth but responsive
|
|
340
|
+
return current + (target - current) * speed;
|
|
341
|
+
});
|
|
342
|
+
// Apply a gentle fade-in for the first 0.3 seconds
|
|
343
|
+
const fadeIn = Math.min(1, elapsed / 0.3);
|
|
344
|
+
const minHeight = 8;
|
|
345
|
+
const maxHeight = 70;
|
|
346
|
+
bars.forEach((bar, i) => {
|
|
347
|
+
// Small random jitter for micro-variation
|
|
348
|
+
const microJitter = 0.95 + Math.random() * 0.1; // 0.95 to 1.05
|
|
349
|
+
const level = Math.max(0.05, Math.min(1, currentLevels[i] * fadeIn * microJitter));
|
|
350
|
+
const height = minHeight + level * (maxHeight - minHeight);
|
|
351
|
+
bar.style.height = `${Math.max(minHeight, height)}px`;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Reset to idle state
|
|
356
|
+
bars.forEach((bar) => {
|
|
357
|
+
bar.style.height = "15px";
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
rafId = requestAnimationFrame(updateAudioBars);
|
|
361
|
+
};
|
|
362
|
+
const startAudioAnalysis = async () => {
|
|
363
|
+
try {
|
|
364
|
+
if (!audioContext) {
|
|
365
|
+
audioContext = new (window.AudioContext ||
|
|
366
|
+
window.webkitAudioContext)();
|
|
367
|
+
}
|
|
368
|
+
if (audioContext.state === "suspended") {
|
|
369
|
+
await audioContext.resume();
|
|
370
|
+
}
|
|
371
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
372
|
+
analyzer = audioContext.createAnalyser();
|
|
373
|
+
analyzer.fftSize = 256;
|
|
374
|
+
analyzer.smoothingTimeConstant = 0.7;
|
|
375
|
+
source = audioContext.createMediaStreamSource(stream);
|
|
376
|
+
source.connect(analyzer);
|
|
377
|
+
dataArray = new Uint8Array(analyzer.fftSize);
|
|
378
|
+
updateAudioBars();
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
console.error("Audio analysis setup failed:", e);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const stopAudioAnalysis = () => {
|
|
385
|
+
if (rafId)
|
|
386
|
+
cancelAnimationFrame(rafId);
|
|
387
|
+
if (source) {
|
|
388
|
+
source.disconnect();
|
|
389
|
+
source.mediaStream.getTracks().forEach((t) => t.stop());
|
|
390
|
+
source = null;
|
|
391
|
+
}
|
|
392
|
+
// Keep context alive if possible, or close it? React component closes it.
|
|
393
|
+
// We can keep it for reuse or close. Let's keep it simple.
|
|
394
|
+
};
|
|
395
|
+
// Create header
|
|
396
|
+
const createHeader = () => {
|
|
397
|
+
const header = document.createElement("div");
|
|
398
|
+
header.style.cssText = `
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: space-between;
|
|
402
|
+
padding: 1rem;
|
|
403
|
+
border-bottom: 1px solid ${aeroTheme.colors.neutral[200]};
|
|
404
|
+
background: white;
|
|
405
|
+
position: relative;
|
|
406
|
+
`;
|
|
407
|
+
// Close button on the left
|
|
408
|
+
const closeButton = document.createElement("button");
|
|
409
|
+
const chevronIcon = Icons.ChevronDown("md");
|
|
410
|
+
closeButton.appendChild(chevronIcon);
|
|
411
|
+
closeButton.style.cssText = `
|
|
412
|
+
font-size: 1.25rem;
|
|
413
|
+
color: ${aeroTheme.colors.text.secondary};
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
padding: 0.25rem;
|
|
416
|
+
transition: ${aeroTheme.transitions.fast};
|
|
417
|
+
background: transparent;
|
|
418
|
+
border: none;
|
|
419
|
+
`;
|
|
420
|
+
closeButton.addEventListener("click", (e) => {
|
|
421
|
+
e.stopPropagation();
|
|
422
|
+
handleClose();
|
|
423
|
+
});
|
|
424
|
+
// Title section centered - matches React version
|
|
425
|
+
const titleSection = document.createElement("div");
|
|
426
|
+
titleSection.style.cssText = `
|
|
427
|
+
position: absolute;
|
|
428
|
+
left: 50%;
|
|
429
|
+
transform: translateX(-50%);
|
|
430
|
+
display: flex;
|
|
431
|
+
align-items: center;
|
|
432
|
+
gap: 0.5rem;
|
|
433
|
+
font-size: ${aeroTheme.typography.fontSize.base};
|
|
434
|
+
font-weight: ${aeroTheme.typography.fontWeight.semibold};
|
|
435
|
+
color: ${aeroTheme.colors.text.primary};
|
|
436
|
+
`;
|
|
437
|
+
titleSection.id = "convai-widget-title";
|
|
438
|
+
// Settings button on the right
|
|
439
|
+
const settingsButton = document.createElement("button");
|
|
440
|
+
const moreIcon = Icons.MoreVertical("md");
|
|
441
|
+
settingsButton.appendChild(moreIcon);
|
|
442
|
+
settingsButton.style.cssText = `
|
|
443
|
+
font-size: 1.5rem;
|
|
444
|
+
color: ${aeroTheme.colors.text.secondary};
|
|
445
|
+
cursor: pointer;
|
|
446
|
+
padding: 0.25rem;
|
|
447
|
+
transition: ${aeroTheme.transitions.fast};
|
|
448
|
+
background: transparent;
|
|
449
|
+
border: none;
|
|
450
|
+
margin-left: auto;
|
|
451
|
+
`;
|
|
452
|
+
settingsButton.addEventListener("click", (e) => {
|
|
453
|
+
e.stopPropagation();
|
|
454
|
+
handleSettingsToggle();
|
|
455
|
+
});
|
|
456
|
+
header.appendChild(closeButton);
|
|
457
|
+
header.appendChild(titleSection);
|
|
458
|
+
header.appendChild(settingsButton);
|
|
459
|
+
return header;
|
|
460
|
+
};
|
|
461
|
+
// Update header with character info
|
|
462
|
+
const updateHeader = () => {
|
|
463
|
+
const titleSection = document.getElementById("convai-widget-title");
|
|
464
|
+
if (!titleSection)
|
|
465
|
+
return;
|
|
466
|
+
titleSection.innerHTML = "";
|
|
467
|
+
if (characterImage) {
|
|
468
|
+
const img = document.createElement("img");
|
|
469
|
+
img.src = characterImage;
|
|
470
|
+
img.alt = characterName;
|
|
471
|
+
img.style.cssText = `
|
|
472
|
+
width: 1.5rem;
|
|
473
|
+
height: 1.5rem;
|
|
474
|
+
border-radius: 50%;
|
|
475
|
+
object-fit: cover;
|
|
476
|
+
border: 1.5px solid ${getBotStatusColor().color};
|
|
477
|
+
box-shadow: 0 0 6px ${getBotStatusColor().color}40;
|
|
478
|
+
transition: all 0.3s ease;
|
|
479
|
+
`;
|
|
480
|
+
titleSection.appendChild(img);
|
|
481
|
+
}
|
|
482
|
+
const nameSpan = document.createElement("span");
|
|
483
|
+
nameSpan.textContent = characterName;
|
|
484
|
+
titleSection.appendChild(nameSpan);
|
|
485
|
+
// Mute button
|
|
486
|
+
const muteButton = document.createElement("button");
|
|
487
|
+
muteButton.innerHTML = "";
|
|
488
|
+
const volumeIcon = isMuted
|
|
489
|
+
? Icons.VolumeMute("sm")
|
|
490
|
+
: Icons.VolumeHigh("sm");
|
|
491
|
+
volumeIcon.style.color = isMuted ? "#919EABA6" : "#0E7360";
|
|
492
|
+
muteButton.appendChild(volumeIcon);
|
|
493
|
+
muteButton.style.cssText = `
|
|
494
|
+
cursor: pointer;
|
|
495
|
+
background: transparent;
|
|
496
|
+
border: none;
|
|
497
|
+
box-shadow: none;
|
|
498
|
+
width: auto;
|
|
499
|
+
height: auto;
|
|
500
|
+
padding: 0;
|
|
501
|
+
display: inline-flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
margin-left: 0.5rem;
|
|
504
|
+
outline: none;
|
|
505
|
+
transition: transform 0.1s ease-out;
|
|
506
|
+
`;
|
|
507
|
+
muteButton.addEventListener("click", (e) => {
|
|
508
|
+
e.stopPropagation();
|
|
509
|
+
handleToggleMute();
|
|
510
|
+
});
|
|
511
|
+
muteButton.addEventListener("mouseenter", () => {
|
|
512
|
+
muteButton.style.transform = "scale(1.1)";
|
|
513
|
+
});
|
|
514
|
+
muteButton.addEventListener("mouseleave", () => {
|
|
515
|
+
muteButton.style.transform = "scale(1)";
|
|
516
|
+
});
|
|
517
|
+
titleSection.appendChild(muteButton);
|
|
518
|
+
// Voice Mode Badge
|
|
519
|
+
if (isVoiceMode) {
|
|
520
|
+
const voiceBadge = document.createElement("span");
|
|
521
|
+
voiceBadge.textContent = "VOICE";
|
|
522
|
+
voiceBadge.style.cssText = `
|
|
523
|
+
font-size: 10px;
|
|
524
|
+
color: ${aeroTheme.colors.convai.light};
|
|
525
|
+
font-weight: 500;
|
|
526
|
+
margin-left: 8px;
|
|
527
|
+
padding: 2px 6px;
|
|
528
|
+
border-radius: 4px;
|
|
529
|
+
background-color: ${aeroTheme.colors.convai.light}20;
|
|
530
|
+
`;
|
|
531
|
+
titleSection.appendChild(voiceBadge);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
// Create message list
|
|
535
|
+
const createMessageList = () => {
|
|
536
|
+
const list = document.createElement("div");
|
|
537
|
+
list.id = "convai-message-list";
|
|
538
|
+
list.style.cssText = `
|
|
539
|
+
display: flex;
|
|
540
|
+
flex-direction: column;
|
|
541
|
+
gap: 0.75rem;
|
|
542
|
+
min-height: 100%;
|
|
543
|
+
`;
|
|
544
|
+
return list;
|
|
545
|
+
};
|
|
546
|
+
// Create footer
|
|
547
|
+
const createFooter = () => {
|
|
548
|
+
const footer = document.createElement("div");
|
|
549
|
+
footer.style.cssText = `
|
|
550
|
+
padding: 1rem;
|
|
551
|
+
border-top: 1px solid ${aeroTheme.colors.neutral[200]};
|
|
552
|
+
background: white;
|
|
553
|
+
display: flex;
|
|
554
|
+
gap: 0.5rem;
|
|
555
|
+
align-items: center;
|
|
556
|
+
position: relative;
|
|
557
|
+
`;
|
|
558
|
+
// Voice Mode Exit Button (Initially hidden)
|
|
559
|
+
const voiceExitButton = document.createElement("button");
|
|
560
|
+
voiceExitButton.id = "convai-voice-exit-btn";
|
|
561
|
+
const exitIcon = Icons.Waveform("md");
|
|
562
|
+
voiceExitButton.appendChild(exitIcon);
|
|
563
|
+
voiceExitButton.style.cssText = `
|
|
564
|
+
width: 2.25rem;
|
|
565
|
+
height: 2.25rem;
|
|
566
|
+
border-radius: 50%;
|
|
567
|
+
background: ${aeroTheme.colors.error[500]};
|
|
568
|
+
color: white;
|
|
569
|
+
display: none; /* Hidden by default */
|
|
570
|
+
align-items: center;
|
|
571
|
+
justify-content: center;
|
|
572
|
+
cursor: pointer;
|
|
573
|
+
margin: 0 auto;
|
|
574
|
+
border: none;
|
|
575
|
+
`;
|
|
576
|
+
voiceExitButton.addEventListener("click", async () => {
|
|
577
|
+
convaiClient.sendInterruptMessage();
|
|
578
|
+
await convaiClient.audioControls.muteAudio(); // Mute on exit
|
|
579
|
+
isVoiceMode = false;
|
|
580
|
+
updateVoiceMode();
|
|
581
|
+
});
|
|
582
|
+
footer.appendChild(voiceExitButton);
|
|
583
|
+
// Standard Footer Content (Mic + Input)
|
|
584
|
+
const standardContent = document.createElement("div");
|
|
585
|
+
standardContent.id = "convai-footer-standard";
|
|
586
|
+
standardContent.style.cssText = `
|
|
587
|
+
display: flex;
|
|
588
|
+
gap: 0.5rem;
|
|
589
|
+
align-items: center;
|
|
590
|
+
width: 100%;
|
|
591
|
+
`;
|
|
592
|
+
// Mic button
|
|
593
|
+
const micButton = document.createElement("button");
|
|
594
|
+
micButton.id = "convai-mic-button";
|
|
595
|
+
const micIcon = Icons.Mic("md");
|
|
596
|
+
micButton.appendChild(micIcon);
|
|
597
|
+
const initialMicBackground = convaiClient.audioControls.isAudioMuted
|
|
598
|
+
? aeroTheme.colors.error[500]
|
|
599
|
+
: aeroTheme.colors.text.primary;
|
|
600
|
+
micButton.style.cssText = `
|
|
601
|
+
width: 2.25rem;
|
|
602
|
+
height: 2.25rem;
|
|
603
|
+
border-radius: 50%;
|
|
604
|
+
background: ${initialMicBackground};
|
|
605
|
+
color: white;
|
|
606
|
+
display: flex;
|
|
607
|
+
align-items: center;
|
|
608
|
+
justify-content: center;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
transition: background-color 0.2s ease-out, transform 0.15s ease-out, box-shadow 0.2s ease-out;
|
|
611
|
+
flex-shrink: 0;
|
|
612
|
+
border: none;
|
|
613
|
+
transform: scale(1);
|
|
614
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
615
|
+
`;
|
|
616
|
+
// Add hover and active effects
|
|
617
|
+
micButton.addEventListener("mouseenter", () => {
|
|
618
|
+
const isMuted = convaiClient.audioControls.isAudioMuted;
|
|
619
|
+
micButton.style.transform = "scale(1.05)";
|
|
620
|
+
if (isMuted) {
|
|
621
|
+
// Red button gets darker on hover
|
|
622
|
+
micButton.style.background = "#dc2626"; // Darker red
|
|
623
|
+
micButton.style.boxShadow = "0 4px 8px rgba(239, 68, 68, 0.3)";
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
// Black button gets slightly lighter on hover
|
|
627
|
+
micButton.style.background = "#374151"; // Lighter dark
|
|
628
|
+
micButton.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
micButton.addEventListener("mouseleave", () => {
|
|
632
|
+
const isMuted = convaiClient.audioControls.isAudioMuted;
|
|
633
|
+
micButton.style.transform = "scale(1)";
|
|
634
|
+
micButton.style.background = isMuted
|
|
635
|
+
? aeroTheme.colors.error[500]
|
|
636
|
+
: aeroTheme.colors.text.primary;
|
|
637
|
+
micButton.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
|
|
638
|
+
});
|
|
639
|
+
micButton.addEventListener("mousedown", () => {
|
|
640
|
+
micButton.style.transform = "scale(0.95)";
|
|
641
|
+
});
|
|
642
|
+
micButton.addEventListener("mouseup", () => {
|
|
643
|
+
micButton.style.transform = "scale(1.05)";
|
|
644
|
+
});
|
|
645
|
+
micButton.addEventListener("click", handleMicToggle);
|
|
646
|
+
standardContent.appendChild(micButton);
|
|
647
|
+
// Input container
|
|
648
|
+
const inputContainer = document.createElement("div");
|
|
649
|
+
inputContainer.style.cssText = `
|
|
650
|
+
flex: 1;
|
|
651
|
+
position: relative;
|
|
652
|
+
display: flex;
|
|
653
|
+
align-items: center;
|
|
654
|
+
`;
|
|
655
|
+
inputElement = document.createElement("input");
|
|
656
|
+
inputElement.type = "text";
|
|
657
|
+
inputElement.placeholder = "Conversation";
|
|
658
|
+
inputElement.style.cssText = `
|
|
659
|
+
width: 100%;
|
|
660
|
+
padding: 0.75rem 3rem 0.75rem 1rem;
|
|
661
|
+
border-radius: ${aeroTheme.borderRadius.full};
|
|
662
|
+
border: 1px solid ${aeroTheme.colors.neutral[300]};
|
|
663
|
+
background: ${aeroTheme.colors.glass.medium};
|
|
664
|
+
color: ${aeroTheme.colors.text.primary};
|
|
665
|
+
font-size: ${aeroTheme.typography.fontSize.sm};
|
|
666
|
+
transition: ${aeroTheme.transitions.fast};
|
|
667
|
+
outline: none;
|
|
668
|
+
`;
|
|
669
|
+
inputElement.addEventListener("input", (e) => {
|
|
670
|
+
inputValue = e.target.value;
|
|
671
|
+
updateSendButton();
|
|
672
|
+
});
|
|
673
|
+
inputElement.addEventListener("keypress", (e) => {
|
|
674
|
+
if (e.key === "Enter")
|
|
675
|
+
handleSend();
|
|
676
|
+
});
|
|
677
|
+
// Send button
|
|
678
|
+
const sendButton = document.createElement("button");
|
|
679
|
+
sendButton.id = "convai-send-button";
|
|
680
|
+
const sendIcon = Icons.Send("md");
|
|
681
|
+
sendButton.appendChild(sendIcon);
|
|
682
|
+
sendButton.style.cssText = `
|
|
683
|
+
position: absolute;
|
|
684
|
+
right: 0.375rem;
|
|
685
|
+
width: 2.25rem;
|
|
686
|
+
height: 2.25rem;
|
|
687
|
+
border-radius: 50%;
|
|
688
|
+
background: transparent; /* Initial transparent for voice toggle */
|
|
689
|
+
color: ${aeroTheme.colors.text.primary};
|
|
690
|
+
display: flex;
|
|
691
|
+
align-items: center;
|
|
692
|
+
justify-content: center;
|
|
693
|
+
cursor: pointer;
|
|
694
|
+
transition: ${aeroTheme.transitions.fast};
|
|
695
|
+
border: none;
|
|
696
|
+
`;
|
|
697
|
+
sendButton.addEventListener("click", async () => {
|
|
698
|
+
if (inputValue.length > 0) {
|
|
699
|
+
handleSend();
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
// Toggle Voice Mode
|
|
703
|
+
convaiClient.sendInterruptMessage();
|
|
704
|
+
await convaiClient.audioControls.unmuteAudio(); // Unmute on enter
|
|
705
|
+
isVoiceMode = true;
|
|
706
|
+
updateVoiceMode();
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
inputContainer.appendChild(inputElement);
|
|
710
|
+
inputContainer.appendChild(sendButton);
|
|
711
|
+
standardContent.appendChild(inputContainer);
|
|
712
|
+
footer.appendChild(standardContent);
|
|
713
|
+
return footer;
|
|
714
|
+
};
|
|
715
|
+
// Create settings tray - Matches React SettingsTray component exactly
|
|
716
|
+
const createSettingsTray = () => {
|
|
717
|
+
const tray = document.createElement("div");
|
|
718
|
+
tray.setAttribute("data-settings-tray", "true");
|
|
719
|
+
tray.style.cssText = `
|
|
720
|
+
position: absolute;
|
|
721
|
+
top: 60px;
|
|
722
|
+
right: 16px;
|
|
723
|
+
background: white;
|
|
724
|
+
border-radius: ${aeroTheme.borderRadius.xl};
|
|
725
|
+
box-shadow: ${aeroTheme.shadows.xl};
|
|
726
|
+
padding: 0;
|
|
727
|
+
display: none;
|
|
728
|
+
flex-direction: row;
|
|
729
|
+
align-items: center;
|
|
730
|
+
gap: 0;
|
|
731
|
+
z-index: 9999;
|
|
732
|
+
min-width: auto;
|
|
733
|
+
transform-origin: top right;
|
|
734
|
+
animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
735
|
+
overflow: hidden;
|
|
736
|
+
`;
|
|
737
|
+
// Add animation keyframes if not present
|
|
738
|
+
if (!document.getElementById("convai-widget-keyframes")) {
|
|
739
|
+
const style = document.createElement("style");
|
|
740
|
+
style.id = "convai-widget-keyframes";
|
|
741
|
+
style.textContent = `
|
|
742
|
+
@keyframes popIn {
|
|
743
|
+
from { opacity: 0; transform: scale(0.95) translateY(-10px); }
|
|
744
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
745
|
+
}
|
|
746
|
+
`;
|
|
747
|
+
document.head.appendChild(style);
|
|
748
|
+
}
|
|
749
|
+
// Horizontal Settings Row Container
|
|
750
|
+
const settingsRow = document.createElement("div");
|
|
751
|
+
settingsRow.style.cssText = `
|
|
752
|
+
display: flex;
|
|
753
|
+
flex-direction: row;
|
|
754
|
+
align-items: center;
|
|
755
|
+
padding: 8px;
|
|
756
|
+
gap: 4px;
|
|
757
|
+
`;
|
|
758
|
+
const createOption = (icon, label, onClick, isActive = false, isDestructive = false) => {
|
|
759
|
+
const btn = document.createElement("div");
|
|
760
|
+
btn.style.cssText = `
|
|
761
|
+
display: flex;
|
|
762
|
+
flex-direction: column;
|
|
763
|
+
align-items: center;
|
|
764
|
+
gap: 4px;
|
|
765
|
+
padding: 8px 12px;
|
|
766
|
+
cursor: pointer;
|
|
767
|
+
border-radius: ${aeroTheme.borderRadius.md};
|
|
768
|
+
background-color: ${isActive
|
|
769
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
770
|
+
: isDestructive
|
|
771
|
+
? "rgba(239, 68, 68, 0.1)"
|
|
772
|
+
: "transparent"};
|
|
773
|
+
color: ${isActive
|
|
774
|
+
? "#10b981"
|
|
775
|
+
: isDestructive
|
|
776
|
+
? "#ef4444"
|
|
777
|
+
: aeroTheme.colors.text.primary};
|
|
778
|
+
transition: ${aeroTheme.transitions.fast};
|
|
779
|
+
min-width: 50px;
|
|
780
|
+
`;
|
|
781
|
+
// Icon container
|
|
782
|
+
const iconContainer = document.createElement("div");
|
|
783
|
+
iconContainer.appendChild(icon);
|
|
784
|
+
btn.appendChild(iconContainer);
|
|
785
|
+
const span = document.createElement("span");
|
|
786
|
+
span.textContent = label;
|
|
787
|
+
span.style.cssText = `
|
|
788
|
+
font-size: 10px;
|
|
789
|
+
font-weight: 500;
|
|
790
|
+
`;
|
|
791
|
+
btn.appendChild(span);
|
|
792
|
+
btn.addEventListener("click", (e) => {
|
|
793
|
+
e.stopPropagation();
|
|
794
|
+
onClick();
|
|
795
|
+
});
|
|
796
|
+
btn.addEventListener("mouseover", () => {
|
|
797
|
+
btn.style.transform = "scale(1.02)";
|
|
798
|
+
if (!isActive && !isDestructive) {
|
|
799
|
+
btn.style.backgroundColor = aeroTheme.colors.neutral[100];
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
btn.addEventListener("mouseout", () => {
|
|
803
|
+
btn.style.transform = "scale(1)";
|
|
804
|
+
btn.style.backgroundColor = isActive
|
|
805
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
806
|
+
: isDestructive
|
|
807
|
+
? "rgba(239, 68, 68, 0.1)"
|
|
808
|
+
: "transparent";
|
|
809
|
+
});
|
|
810
|
+
return btn;
|
|
811
|
+
};
|
|
812
|
+
// Reset
|
|
813
|
+
const resetIcon = Icons.Redo("md");
|
|
814
|
+
resetIcon.style.width = "18px";
|
|
815
|
+
resetIcon.style.height = "18px";
|
|
816
|
+
settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
|
|
817
|
+
// Video
|
|
818
|
+
if (convaiClient.connectionType === "video" && showVideo) {
|
|
819
|
+
const videoIcon = isVideoVisible
|
|
820
|
+
? Icons.Video("md")
|
|
821
|
+
: Icons.VideoOff("md");
|
|
822
|
+
videoIcon.style.width = "18px";
|
|
823
|
+
videoIcon.style.height = "18px";
|
|
824
|
+
const videoBtn = createOption(videoIcon, "Video", handleToggleVideo, isVideoVisible);
|
|
825
|
+
videoBtn.id = "convai-settings-video-btn";
|
|
826
|
+
settingsRow.appendChild(videoBtn);
|
|
827
|
+
}
|
|
828
|
+
// Screen Share
|
|
829
|
+
if (convaiClient.connectionType === "video" && showScreenShare) {
|
|
830
|
+
const isSharing = convaiClient.screenShareControls.isScreenShareActive;
|
|
831
|
+
const shareIcon = isSharing
|
|
832
|
+
? Icons.StopScreenShare("md")
|
|
833
|
+
: Icons.ScreenShare("md");
|
|
834
|
+
shareIcon.style.width = "18px";
|
|
835
|
+
shareIcon.style.height = "18px";
|
|
836
|
+
const shareBtn = createOption(shareIcon, "Screen", handleToggleScreenShare, isSharing);
|
|
837
|
+
shareBtn.id = "convai-settings-share-btn";
|
|
838
|
+
settingsRow.appendChild(shareBtn);
|
|
839
|
+
}
|
|
840
|
+
// Disconnect
|
|
841
|
+
const disconnectIcon = document.createElement("span");
|
|
842
|
+
disconnectIcon.innerHTML = "⏻";
|
|
843
|
+
disconnectIcon.style.fontSize = "18px";
|
|
844
|
+
settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
|
|
845
|
+
tray.appendChild(settingsRow);
|
|
846
|
+
return tray;
|
|
847
|
+
};
|
|
848
|
+
// Update settings tray content to reflect current connection state
|
|
849
|
+
const updateSettingsTray = () => {
|
|
850
|
+
const settingsRow = settingsTray.querySelector("div");
|
|
851
|
+
if (!settingsRow)
|
|
852
|
+
return;
|
|
853
|
+
// Clear existing content
|
|
854
|
+
settingsRow.innerHTML = "";
|
|
855
|
+
const createOption = (icon, label, onClick, isActive = false, isDestructive = false) => {
|
|
856
|
+
const btn = document.createElement("div");
|
|
857
|
+
btn.style.cssText = `
|
|
858
|
+
display: flex;
|
|
859
|
+
flex-direction: column;
|
|
860
|
+
align-items: center;
|
|
861
|
+
gap: 4px;
|
|
862
|
+
padding: 8px 12px;
|
|
863
|
+
cursor: pointer;
|
|
864
|
+
border-radius: ${aeroTheme.borderRadius.md};
|
|
865
|
+
background-color: ${isActive
|
|
866
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
867
|
+
: isDestructive
|
|
868
|
+
? "rgba(239, 68, 68, 0.1)"
|
|
869
|
+
: "transparent"};
|
|
870
|
+
color: ${isActive
|
|
871
|
+
? "#10b981"
|
|
872
|
+
: isDestructive
|
|
873
|
+
? "#ef4444"
|
|
874
|
+
: aeroTheme.colors.text.primary};
|
|
875
|
+
transition: ${aeroTheme.transitions.fast};
|
|
876
|
+
min-width: 50px;
|
|
877
|
+
`;
|
|
878
|
+
// Icon container
|
|
879
|
+
const iconContainer = document.createElement("div");
|
|
880
|
+
iconContainer.appendChild(icon);
|
|
881
|
+
btn.appendChild(iconContainer);
|
|
882
|
+
const span = document.createElement("span");
|
|
883
|
+
span.textContent = label;
|
|
884
|
+
span.style.cssText = `
|
|
885
|
+
font-size: 10px;
|
|
886
|
+
font-weight: 500;
|
|
887
|
+
`;
|
|
888
|
+
btn.appendChild(span);
|
|
889
|
+
btn.addEventListener("click", (e) => {
|
|
890
|
+
e.stopPropagation();
|
|
891
|
+
onClick();
|
|
892
|
+
});
|
|
893
|
+
btn.addEventListener("mouseover", () => {
|
|
894
|
+
btn.style.transform = "scale(1.02)";
|
|
895
|
+
if (!isActive && !isDestructive) {
|
|
896
|
+
btn.style.backgroundColor = aeroTheme.colors.neutral[100];
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
btn.addEventListener("mouseout", () => {
|
|
900
|
+
btn.style.transform = "scale(1)";
|
|
901
|
+
btn.style.backgroundColor = isActive
|
|
902
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
903
|
+
: isDestructive
|
|
904
|
+
? "rgba(239, 68, 68, 0.1)"
|
|
905
|
+
: "transparent";
|
|
906
|
+
});
|
|
907
|
+
return btn;
|
|
908
|
+
};
|
|
909
|
+
// Reset
|
|
910
|
+
const resetIcon = Icons.Redo("md");
|
|
911
|
+
resetIcon.style.width = "18px";
|
|
912
|
+
resetIcon.style.height = "18px";
|
|
913
|
+
settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
|
|
914
|
+
// Video - Always show if showVideo is true and connection type is video
|
|
915
|
+
if (convaiClient.connectionType === "video" && showVideo) {
|
|
916
|
+
const videoIcon = isVideoVisible
|
|
917
|
+
? Icons.Video("md")
|
|
918
|
+
: Icons.VideoOff("md");
|
|
919
|
+
videoIcon.style.width = "18px";
|
|
920
|
+
videoIcon.style.height = "18px";
|
|
921
|
+
const videoBtn = createOption(videoIcon, "Video", handleToggleVideo, isVideoVisible);
|
|
922
|
+
videoBtn.id = "convai-settings-video-btn";
|
|
923
|
+
settingsRow.appendChild(videoBtn);
|
|
924
|
+
}
|
|
925
|
+
// Screen Share - Always show if showScreenShare is true and connection type is video
|
|
926
|
+
if (convaiClient.connectionType === "video" && showScreenShare) {
|
|
927
|
+
const isSharing = convaiClient.screenShareControls.isScreenShareActive;
|
|
928
|
+
const shareIcon = isSharing
|
|
929
|
+
? Icons.StopScreenShare("md")
|
|
930
|
+
: Icons.ScreenShare("md");
|
|
931
|
+
shareIcon.style.width = "18px";
|
|
932
|
+
shareIcon.style.height = "18px";
|
|
933
|
+
const shareBtn = createOption(shareIcon, "Screen", handleToggleScreenShare, isSharing);
|
|
934
|
+
shareBtn.id = "convai-settings-share-btn";
|
|
935
|
+
settingsRow.appendChild(shareBtn);
|
|
936
|
+
}
|
|
937
|
+
// Disconnect
|
|
938
|
+
const disconnectIcon = document.createElement("span");
|
|
939
|
+
disconnectIcon.innerHTML = "⏻";
|
|
940
|
+
disconnectIcon.style.fontSize = "18px";
|
|
941
|
+
settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
|
|
942
|
+
};
|
|
943
|
+
// Create floating video - Matches React FloatingVideo component
|
|
944
|
+
const createFloatingVideo = () => {
|
|
945
|
+
const container = document.createElement("div");
|
|
946
|
+
container.id = "floating-video-container";
|
|
947
|
+
container.style.cssText = `
|
|
948
|
+
position: fixed;
|
|
949
|
+
left: 20px;
|
|
950
|
+
top: 20px;
|
|
951
|
+
z-index: 10000;
|
|
952
|
+
width: 320px;
|
|
953
|
+
border-radius: ${aeroTheme.borderRadius.lg};
|
|
954
|
+
overflow: hidden;
|
|
955
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
956
|
+
cursor: grab;
|
|
957
|
+
display: none;
|
|
958
|
+
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
959
|
+
`;
|
|
960
|
+
const wrapper = document.createElement("div");
|
|
961
|
+
wrapper.style.cssText = `
|
|
962
|
+
background: rgba(15, 23, 42, 0.95);
|
|
963
|
+
backdrop-filter: blur(20px);
|
|
964
|
+
border: 1px solid ${aeroTheme.colors.neutral[700]};
|
|
965
|
+
border-radius: ${aeroTheme.borderRadius.lg};
|
|
966
|
+
overflow: hidden;
|
|
967
|
+
`;
|
|
968
|
+
// Header
|
|
969
|
+
const header = document.createElement("div");
|
|
970
|
+
header.style.cssText = `
|
|
971
|
+
display: flex;
|
|
972
|
+
align-items: center;
|
|
973
|
+
justify-content: space-between;
|
|
974
|
+
padding: 8px 12px;
|
|
975
|
+
background: rgba(0, 0, 0, 0.3);
|
|
976
|
+
border-bottom: 1px solid ${aeroTheme.colors.neutral[700]};
|
|
977
|
+
`;
|
|
978
|
+
const headerLeft = document.createElement("div");
|
|
979
|
+
headerLeft.style.cssText = `
|
|
980
|
+
display: flex;
|
|
981
|
+
align-items: center;
|
|
982
|
+
gap: 8px;
|
|
983
|
+
color: ${aeroTheme.colors.neutral[300]};
|
|
984
|
+
font-size: 12px;
|
|
985
|
+
font-weight: 500;
|
|
986
|
+
`;
|
|
987
|
+
const dragIcon = Icons.DragIndicator("sm");
|
|
988
|
+
dragIcon.style.width = "16px";
|
|
989
|
+
dragIcon.style.height = "16px";
|
|
990
|
+
headerLeft.appendChild(dragIcon);
|
|
991
|
+
const label = document.createElement("span");
|
|
992
|
+
label.textContent = "Your Camera";
|
|
993
|
+
headerLeft.appendChild(label);
|
|
994
|
+
const closeButton = document.createElement("button");
|
|
995
|
+
closeButton.innerHTML = "";
|
|
996
|
+
const closeIcon = Icons.Close("md");
|
|
997
|
+
closeIcon.style.width = "20px";
|
|
998
|
+
closeIcon.style.height = "20px";
|
|
999
|
+
closeButton.appendChild(closeIcon);
|
|
1000
|
+
closeButton.style.cssText = `
|
|
1001
|
+
background: transparent;
|
|
1002
|
+
border: none;
|
|
1003
|
+
color: ${aeroTheme.colors.neutral[400]};
|
|
1004
|
+
cursor: pointer;
|
|
1005
|
+
display: flex;
|
|
1006
|
+
align-items: center;
|
|
1007
|
+
justify-content: center;
|
|
1008
|
+
padding: 4px;
|
|
1009
|
+
transition: transform 0.1s ease-out;
|
|
1010
|
+
`;
|
|
1011
|
+
closeButton.addEventListener("click", (e) => {
|
|
1012
|
+
e.stopPropagation();
|
|
1013
|
+
convaiClient.videoControls.disableVideo();
|
|
1014
|
+
});
|
|
1015
|
+
closeButton.addEventListener("mouseenter", () => {
|
|
1016
|
+
closeButton.style.transform = "scale(1.1)";
|
|
1017
|
+
});
|
|
1018
|
+
closeButton.addEventListener("mouseleave", () => {
|
|
1019
|
+
closeButton.style.transform = "scale(1)";
|
|
1020
|
+
});
|
|
1021
|
+
header.appendChild(headerLeft);
|
|
1022
|
+
header.appendChild(closeButton);
|
|
1023
|
+
// Video Container
|
|
1024
|
+
const videoContainer = document.createElement("div");
|
|
1025
|
+
videoContainer.style.cssText = `
|
|
1026
|
+
position: relative;
|
|
1027
|
+
width: 100%;
|
|
1028
|
+
padding-bottom: 75%;
|
|
1029
|
+
background: #000;
|
|
1030
|
+
`;
|
|
1031
|
+
const videoWrapper = document.createElement("div");
|
|
1032
|
+
videoWrapper.id = "floating-video-wrapper";
|
|
1033
|
+
videoWrapper.style.cssText = `
|
|
1034
|
+
position: absolute;
|
|
1035
|
+
top: 0;
|
|
1036
|
+
left: 0;
|
|
1037
|
+
width: 100%;
|
|
1038
|
+
height: 100%;
|
|
1039
|
+
`;
|
|
1040
|
+
const videoElement = document.createElement("video");
|
|
1041
|
+
videoElement.id = "floating-video-element";
|
|
1042
|
+
videoElement.autoplay = true;
|
|
1043
|
+
videoElement.playsInline = true;
|
|
1044
|
+
videoElement.muted = true;
|
|
1045
|
+
videoElement.style.cssText = `
|
|
1046
|
+
width: 100%;
|
|
1047
|
+
height: 100%;
|
|
1048
|
+
object-fit: cover;
|
|
1049
|
+
transform: scaleX(-1);
|
|
1050
|
+
`;
|
|
1051
|
+
// Camera off placeholder
|
|
1052
|
+
const cameraOffPlaceholder = document.createElement("div");
|
|
1053
|
+
cameraOffPlaceholder.id = "camera-off-placeholder";
|
|
1054
|
+
cameraOffPlaceholder.style.cssText = `
|
|
1055
|
+
width: 100%;
|
|
1056
|
+
height: 100%;
|
|
1057
|
+
display: flex;
|
|
1058
|
+
flex-direction: column;
|
|
1059
|
+
align-items: center;
|
|
1060
|
+
justify-content: center;
|
|
1061
|
+
color: ${aeroTheme.colors.neutral[400]};
|
|
1062
|
+
gap: 12px;
|
|
1063
|
+
background: #000;
|
|
1064
|
+
`;
|
|
1065
|
+
const cameraOffIcon = Icons.VideoOff("lg");
|
|
1066
|
+
cameraOffIcon.style.width = "48px";
|
|
1067
|
+
cameraOffIcon.style.height = "48px";
|
|
1068
|
+
cameraOffPlaceholder.appendChild(cameraOffIcon);
|
|
1069
|
+
const cameraOffText = document.createElement("span");
|
|
1070
|
+
cameraOffText.textContent = "Camera is off";
|
|
1071
|
+
cameraOffText.style.cssText = `
|
|
1072
|
+
font-size: 14px;
|
|
1073
|
+
text-align: center;
|
|
1074
|
+
`;
|
|
1075
|
+
cameraOffPlaceholder.appendChild(cameraOffText);
|
|
1076
|
+
videoWrapper.appendChild(videoElement);
|
|
1077
|
+
videoWrapper.appendChild(cameraOffPlaceholder);
|
|
1078
|
+
videoContainer.appendChild(videoWrapper);
|
|
1079
|
+
wrapper.appendChild(header);
|
|
1080
|
+
wrapper.appendChild(videoContainer);
|
|
1081
|
+
container.appendChild(wrapper);
|
|
1082
|
+
// Add drag functionality
|
|
1083
|
+
let isDragging = false;
|
|
1084
|
+
let startX = 0;
|
|
1085
|
+
let startY = 0;
|
|
1086
|
+
let startLeft = 20;
|
|
1087
|
+
let startTop = 20;
|
|
1088
|
+
const handleMouseDown = (e) => {
|
|
1089
|
+
if (e.target.closest("button"))
|
|
1090
|
+
return;
|
|
1091
|
+
isDragging = true;
|
|
1092
|
+
startX = e.clientX;
|
|
1093
|
+
startY = e.clientY;
|
|
1094
|
+
const style = window.getComputedStyle(container);
|
|
1095
|
+
startLeft = parseInt(style.left);
|
|
1096
|
+
startTop = parseInt(style.top);
|
|
1097
|
+
container.style.cursor = "grabbing";
|
|
1098
|
+
e.preventDefault();
|
|
1099
|
+
};
|
|
1100
|
+
const handleMouseMove = (e) => {
|
|
1101
|
+
if (!isDragging)
|
|
1102
|
+
return;
|
|
1103
|
+
const deltaX = e.clientX - startX;
|
|
1104
|
+
const deltaY = e.clientY - startY;
|
|
1105
|
+
const newLeft = startLeft + deltaX;
|
|
1106
|
+
const newTop = startTop + deltaY;
|
|
1107
|
+
// Keep within bounds
|
|
1108
|
+
const maxX = window.innerWidth - 320;
|
|
1109
|
+
const maxY = window.innerHeight - 240;
|
|
1110
|
+
const boundedLeft = Math.max(0, Math.min(newLeft, maxX));
|
|
1111
|
+
const boundedTop = Math.max(0, Math.min(newTop, maxY));
|
|
1112
|
+
container.style.left = `${boundedLeft}px`;
|
|
1113
|
+
container.style.top = `${boundedTop}px`;
|
|
1114
|
+
};
|
|
1115
|
+
const handleMouseUp = () => {
|
|
1116
|
+
if (isDragging) {
|
|
1117
|
+
isDragging = false;
|
|
1118
|
+
container.style.cursor = "grab";
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
container.addEventListener("mousedown", handleMouseDown);
|
|
1122
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
1123
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
1124
|
+
return container;
|
|
1125
|
+
};
|
|
1126
|
+
// Event handlers
|
|
1127
|
+
const handleToggle = async () => {
|
|
1128
|
+
if (isOpen) {
|
|
1129
|
+
// Just close if already open
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
// Connect on first click if not already connected/connecting
|
|
1133
|
+
if (!convaiClient.state.isConnected && !convaiClient.state.isConnecting) {
|
|
1134
|
+
try {
|
|
1135
|
+
await convaiClient.connect();
|
|
1136
|
+
setIsOpen(true);
|
|
1137
|
+
}
|
|
1138
|
+
catch (error) {
|
|
1139
|
+
console.error("Failed to connect:", error);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
// Just toggle open/close if already connected
|
|
1144
|
+
setIsOpen(!isOpen);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
const handleClose = () => {
|
|
1148
|
+
setIsOpen(false);
|
|
1149
|
+
};
|
|
1150
|
+
const handleSend = () => {
|
|
1151
|
+
if (inputValue.trim() && convaiClient.state.isConnected) {
|
|
1152
|
+
convaiClient.sendUserTextMessage(inputValue);
|
|
1153
|
+
inputValue = "";
|
|
1154
|
+
inputElement.value = "";
|
|
1155
|
+
updateSendButton();
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
const handleMicToggle = async () => {
|
|
1159
|
+
await convaiClient.audioControls.toggleAudio();
|
|
1160
|
+
updateMicButton();
|
|
1161
|
+
};
|
|
1162
|
+
const handleToggleMute = () => {
|
|
1163
|
+
isMuted = !isMuted;
|
|
1164
|
+
if (convaiClient.room) {
|
|
1165
|
+
const remoteParticipants = Array.from(convaiClient.room.remoteParticipants.values());
|
|
1166
|
+
remoteParticipants.forEach((participant) => {
|
|
1167
|
+
participant.audioTrackPublications.forEach((publication) => {
|
|
1168
|
+
if (publication.track) {
|
|
1169
|
+
publication.track.setMuted(isMuted);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
updateHeader();
|
|
1175
|
+
};
|
|
1176
|
+
const handleReset = async () => {
|
|
1177
|
+
setIsSettingsOpen(false);
|
|
1178
|
+
try {
|
|
1179
|
+
await convaiClient.disconnect();
|
|
1180
|
+
convaiClient.resetSession();
|
|
1181
|
+
isMuted = false;
|
|
1182
|
+
isVoiceMode = false;
|
|
1183
|
+
updateHeader();
|
|
1184
|
+
updateVoiceMode();
|
|
1185
|
+
updateMicButton();
|
|
1186
|
+
updateSendButton();
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
console.error("Failed to reset:", error);
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
const handleDisconnect = async () => {
|
|
1193
|
+
setIsSettingsOpen(false);
|
|
1194
|
+
await convaiClient.disconnect();
|
|
1195
|
+
isMuted = false;
|
|
1196
|
+
isVoiceMode = false;
|
|
1197
|
+
updateHeader();
|
|
1198
|
+
updateVoiceMode();
|
|
1199
|
+
updateMicButton();
|
|
1200
|
+
updateSendButton();
|
|
1201
|
+
};
|
|
1202
|
+
const handleToggleVideo = async () => {
|
|
1203
|
+
if (convaiClient.connectionType !== "video")
|
|
1204
|
+
return;
|
|
1205
|
+
try {
|
|
1206
|
+
if (isVideoVisible) {
|
|
1207
|
+
await convaiClient.videoControls.disableVideo();
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
await convaiClient.videoControls.enableVideo();
|
|
1211
|
+
}
|
|
1212
|
+
// State will be updated via event listener below
|
|
1213
|
+
}
|
|
1214
|
+
catch (e) {
|
|
1215
|
+
console.error("Failed to toggle video:", e);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
const handleToggleScreenShare = async () => {
|
|
1219
|
+
try {
|
|
1220
|
+
await convaiClient.screenShareControls.toggleScreenShare();
|
|
1221
|
+
// State will be updated via event listener above
|
|
1222
|
+
}
|
|
1223
|
+
catch (e) {
|
|
1224
|
+
console.error("Failed to toggle screen share:", e);
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
const handleSettingsToggle = () => {
|
|
1228
|
+
// Regenerate settings tray content BEFORE opening to reflect current connection type
|
|
1229
|
+
if (!isSettingsOpen) {
|
|
1230
|
+
updateSettingsTray();
|
|
1231
|
+
}
|
|
1232
|
+
setIsSettingsOpen(!isSettingsOpen);
|
|
1233
|
+
};
|
|
1234
|
+
// Update functions
|
|
1235
|
+
const setIsOpen = (open) => {
|
|
1236
|
+
isOpen = open;
|
|
1237
|
+
if (open) {
|
|
1238
|
+
morphingContainer.style.width = "400px";
|
|
1239
|
+
morphingContainer.style.height = "600px";
|
|
1240
|
+
morphingContainer.style.borderRadius = aeroTheme.borderRadius.xl;
|
|
1241
|
+
morphingContainer.style.cursor = "default";
|
|
1242
|
+
buttonContent.style.opacity = "0";
|
|
1243
|
+
buttonContent.style.transform = "scale(0.8)";
|
|
1244
|
+
buttonContent.style.pointerEvents = "none";
|
|
1245
|
+
chatContent.style.opacity = "1";
|
|
1246
|
+
chatContent.style.transform = "scale(1)";
|
|
1247
|
+
chatContent.style.pointerEvents = "auto";
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
morphingContainer.style.width = "4rem";
|
|
1251
|
+
morphingContainer.style.height = "4rem";
|
|
1252
|
+
morphingContainer.style.borderRadius = "50%";
|
|
1253
|
+
morphingContainer.style.cursor = "pointer";
|
|
1254
|
+
buttonContent.style.opacity = "1";
|
|
1255
|
+
buttonContent.style.transform = "scale(1)";
|
|
1256
|
+
buttonContent.style.pointerEvents = "auto";
|
|
1257
|
+
chatContent.style.opacity = "0";
|
|
1258
|
+
chatContent.style.transform = "scale(0.8)";
|
|
1259
|
+
chatContent.style.pointerEvents = "none";
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
const setIsSettingsOpen = (open) => {
|
|
1263
|
+
isSettingsOpen = open;
|
|
1264
|
+
settingsTray.style.display = open ? "flex" : "none";
|
|
1265
|
+
};
|
|
1266
|
+
const updateSendButton = () => {
|
|
1267
|
+
const sendButton = document.getElementById("convai-send-button");
|
|
1268
|
+
if (!sendButton)
|
|
1269
|
+
return;
|
|
1270
|
+
// Clear previous content
|
|
1271
|
+
sendButton.innerHTML = "";
|
|
1272
|
+
if (inputValue.length > 0) {
|
|
1273
|
+
// Show Send Button
|
|
1274
|
+
sendButton.appendChild(Icons.Send("md"));
|
|
1275
|
+
sendButton.style.background = aeroTheme.colors.text.primary;
|
|
1276
|
+
sendButton.style.color = "white";
|
|
1277
|
+
sendButton.style.border = "none";
|
|
1278
|
+
sendButton.title = "Send";
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
// Show Voice Mode Toggle
|
|
1282
|
+
sendButton.appendChild(Icons.Waveform("md"));
|
|
1283
|
+
sendButton.style.background = "transparent";
|
|
1284
|
+
sendButton.style.color = aeroTheme.colors.text.primary;
|
|
1285
|
+
sendButton.style.border = `1px solid ${aeroTheme.colors.neutral[300]}`;
|
|
1286
|
+
sendButton.title = "Voice Mode";
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const updateVideoTrack = () => {
|
|
1290
|
+
if (!floatingVideo ||
|
|
1291
|
+
!convaiClient.room ||
|
|
1292
|
+
!convaiClient.room.localParticipant)
|
|
1293
|
+
return;
|
|
1294
|
+
const videoEl = floatingVideo.querySelector("#floating-video-element");
|
|
1295
|
+
const placeholder = floatingVideo.querySelector("#camera-off-placeholder");
|
|
1296
|
+
if (!videoEl || !placeholder)
|
|
1297
|
+
return;
|
|
1298
|
+
if (isVideoVisible && !isVoiceMode) {
|
|
1299
|
+
const tracks = Array.from(convaiClient.room.localParticipant.videoTrackPublications.values());
|
|
1300
|
+
const videoPub = tracks.find((t) => t.kind === "video");
|
|
1301
|
+
if (videoPub && videoPub.track) {
|
|
1302
|
+
videoPub.track.attach(videoEl);
|
|
1303
|
+
videoEl.style.display = "block";
|
|
1304
|
+
placeholder.style.display = "none";
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
videoEl.style.display = "none";
|
|
1308
|
+
placeholder.style.display = "flex";
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
else {
|
|
1312
|
+
videoEl.style.display = "none";
|
|
1313
|
+
placeholder.style.display = "flex";
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
const updateVoiceMode = () => {
|
|
1317
|
+
const standardFooter = document.getElementById("convai-footer-standard");
|
|
1318
|
+
const voiceExitBtn = document.getElementById("convai-voice-exit-btn");
|
|
1319
|
+
if (isVoiceMode) {
|
|
1320
|
+
// Show Voice Overlay
|
|
1321
|
+
if (voiceModeOverlay)
|
|
1322
|
+
voiceModeOverlay.style.display = "flex";
|
|
1323
|
+
if (messageListElement)
|
|
1324
|
+
messageListElement.style.display = "none";
|
|
1325
|
+
if (floatingVideo)
|
|
1326
|
+
floatingVideo.style.display = "none";
|
|
1327
|
+
// Footer: Show Exit Button, Hide Standard
|
|
1328
|
+
if (standardFooter)
|
|
1329
|
+
standardFooter.style.display = "none";
|
|
1330
|
+
if (voiceExitBtn)
|
|
1331
|
+
voiceExitBtn.style.display = "flex";
|
|
1332
|
+
if (footerElement)
|
|
1333
|
+
footerElement.style.justifyContent = "center";
|
|
1334
|
+
// Update header to show VOICE badge
|
|
1335
|
+
updateHeader();
|
|
1336
|
+
// Reset animation state for voice mode
|
|
1337
|
+
startTime = Date.now();
|
|
1338
|
+
currentLevels = Array(40).fill(0.05);
|
|
1339
|
+
targetLevels = Array(40).fill(0.05);
|
|
1340
|
+
// Start Audio Analysis
|
|
1341
|
+
startAudioAnalysis();
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
// Hide Overlay
|
|
1345
|
+
if (voiceModeOverlay)
|
|
1346
|
+
voiceModeOverlay.style.display = "none";
|
|
1347
|
+
// Show Video or Message List
|
|
1348
|
+
if (isVideoVisible) {
|
|
1349
|
+
if (messageListElement)
|
|
1350
|
+
messageListElement.style.display = "none";
|
|
1351
|
+
if (floatingVideo)
|
|
1352
|
+
floatingVideo.style.display = "block";
|
|
1353
|
+
updateVideoTrack();
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
if (messageListElement)
|
|
1357
|
+
messageListElement.style.display = "flex";
|
|
1358
|
+
if (floatingVideo)
|
|
1359
|
+
floatingVideo.style.display = "none";
|
|
1360
|
+
}
|
|
1361
|
+
// Footer: Show Standard, Hide Exit
|
|
1362
|
+
if (standardFooter)
|
|
1363
|
+
standardFooter.style.display = "flex";
|
|
1364
|
+
if (voiceExitBtn)
|
|
1365
|
+
voiceExitBtn.style.display = "none";
|
|
1366
|
+
if (footerElement)
|
|
1367
|
+
footerElement.style.justifyContent = "flex-start";
|
|
1368
|
+
// Update header to hide VOICE badge
|
|
1369
|
+
updateHeader();
|
|
1370
|
+
// Stop Audio Analysis
|
|
1371
|
+
stopAudioAnalysis();
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
const updateMicButton = () => {
|
|
1375
|
+
// Update mic button appearance based on mute state with visual feedback
|
|
1376
|
+
const micButton = document.getElementById("convai-mic-button");
|
|
1377
|
+
if (micButton) {
|
|
1378
|
+
const isMuted = convaiClient.audioControls.isAudioMuted;
|
|
1379
|
+
const newBackground = isMuted
|
|
1380
|
+
? aeroTheme.colors.error[500]
|
|
1381
|
+
: aeroTheme.colors.text.primary;
|
|
1382
|
+
// Add a brief pulse effect to show the state changed
|
|
1383
|
+
micButton.style.transform = "scale(1.15)";
|
|
1384
|
+
micButton.style.background = newBackground;
|
|
1385
|
+
// Add colored glow based on state
|
|
1386
|
+
if (isMuted) {
|
|
1387
|
+
micButton.style.boxShadow =
|
|
1388
|
+
"0 0 12px rgba(239, 68, 68, 0.6), 0 2px 4px rgba(0, 0, 0, 0.1)";
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
micButton.style.boxShadow =
|
|
1392
|
+
"0 0 12px rgba(16, 185, 129, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)";
|
|
1393
|
+
}
|
|
1394
|
+
// Return to normal size after animation
|
|
1395
|
+
setTimeout(() => {
|
|
1396
|
+
if (micButton) {
|
|
1397
|
+
micButton.style.transform = "scale(1)";
|
|
1398
|
+
micButton.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
|
|
1399
|
+
}
|
|
1400
|
+
}, 200);
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
// Helper for Markdown - matches React MarkdownRenderer.tsx
|
|
1404
|
+
const renderMarkdown = (text, container) => {
|
|
1405
|
+
if (!text)
|
|
1406
|
+
return;
|
|
1407
|
+
// Handle both actual newlines and \n escape sequences
|
|
1408
|
+
const normalizedText = text.replace(/\\n/g, "\n");
|
|
1409
|
+
const lines = normalizedText.split("\n");
|
|
1410
|
+
lines.forEach((line, lineIndex) => {
|
|
1411
|
+
// Process markdown within each line
|
|
1412
|
+
const processLine = (text) => {
|
|
1413
|
+
const parts = [];
|
|
1414
|
+
let remaining = text;
|
|
1415
|
+
// Process bold and italic markdown
|
|
1416
|
+
// Bold: **text** or __text__
|
|
1417
|
+
// Italic: *text* or _text_
|
|
1418
|
+
const markdownRegex = /(\*\*|__)(.*?)\1|\*([^*]+)\*|_([^_]+)_/g;
|
|
1419
|
+
let lastIndex = 0;
|
|
1420
|
+
let match;
|
|
1421
|
+
while ((match = markdownRegex.exec(remaining)) !== null) {
|
|
1422
|
+
// Add text before the match
|
|
1423
|
+
if (match.index > lastIndex) {
|
|
1424
|
+
const textBefore = remaining.substring(lastIndex, match.index);
|
|
1425
|
+
if (textBefore) {
|
|
1426
|
+
parts.push(document.createTextNode(textBefore));
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// Determine if it's bold or italic
|
|
1430
|
+
if (match[1] && match[2]) {
|
|
1431
|
+
// Bold text (** or __) - matches React UserBubble styling
|
|
1432
|
+
const strong = document.createElement("strong");
|
|
1433
|
+
strong.textContent = match[2];
|
|
1434
|
+
strong.style.cssText = `
|
|
1435
|
+
font-weight: 600;
|
|
1436
|
+
color: ${aeroTheme.colors.text.primary};
|
|
1437
|
+
`;
|
|
1438
|
+
parts.push(strong);
|
|
1439
|
+
}
|
|
1440
|
+
else if (match[3]) {
|
|
1441
|
+
// Italic text (*) - matches React UserBubble styling
|
|
1442
|
+
const em = document.createElement("em");
|
|
1443
|
+
em.textContent = match[3];
|
|
1444
|
+
em.style.cssText = `
|
|
1445
|
+
font-style: italic;
|
|
1446
|
+
`;
|
|
1447
|
+
parts.push(em);
|
|
1448
|
+
}
|
|
1449
|
+
else if (match[4]) {
|
|
1450
|
+
// Italic text (_) - matches React UserBubble styling
|
|
1451
|
+
const em = document.createElement("em");
|
|
1452
|
+
em.textContent = match[4];
|
|
1453
|
+
em.style.cssText = `
|
|
1454
|
+
font-style: italic;
|
|
1455
|
+
`;
|
|
1456
|
+
parts.push(em);
|
|
1457
|
+
}
|
|
1458
|
+
lastIndex = match.index + match[0].length;
|
|
1459
|
+
}
|
|
1460
|
+
// Add remaining text
|
|
1461
|
+
if (lastIndex < remaining.length) {
|
|
1462
|
+
const textAfter = remaining.substring(lastIndex);
|
|
1463
|
+
if (textAfter) {
|
|
1464
|
+
parts.push(document.createTextNode(textAfter));
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return parts;
|
|
1468
|
+
};
|
|
1469
|
+
const processedLine = processLine(line);
|
|
1470
|
+
// Handle empty lines (creates paragraph spacing)
|
|
1471
|
+
if (line.trim() === "" && lineIndex > 0) {
|
|
1472
|
+
container.appendChild(document.createElement("br"));
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
// Append the processed line
|
|
1476
|
+
if (processedLine.length > 0) {
|
|
1477
|
+
processedLine.forEach((part) => container.appendChild(part));
|
|
1478
|
+
}
|
|
1479
|
+
else {
|
|
1480
|
+
container.appendChild(document.createTextNode(line));
|
|
1481
|
+
}
|
|
1482
|
+
// Add line break between lines
|
|
1483
|
+
if (lineIndex < lines.length - 1) {
|
|
1484
|
+
container.appendChild(document.createElement("br"));
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
};
|
|
1488
|
+
const updateMessageList = () => {
|
|
1489
|
+
if (!messageListElement)
|
|
1490
|
+
return;
|
|
1491
|
+
const messages = formatMessages();
|
|
1492
|
+
messageListElement.innerHTML = "";
|
|
1493
|
+
if (messages.length === 0) {
|
|
1494
|
+
const emptyState = document.createElement("div");
|
|
1495
|
+
emptyState.style.cssText = `
|
|
1496
|
+
display: flex;
|
|
1497
|
+
flex-direction: column;
|
|
1498
|
+
align-items: center;
|
|
1499
|
+
justify-content: center;
|
|
1500
|
+
height: 100%;
|
|
1501
|
+
color: ${aeroTheme.colors.text.secondary};
|
|
1502
|
+
text-align: center;
|
|
1503
|
+
padding: 2rem;
|
|
1504
|
+
`;
|
|
1505
|
+
emptyState.innerHTML = `
|
|
1506
|
+
<p style="font-weight: 500; margin-bottom: 8px;">Start a conversation</p>
|
|
1507
|
+
<p style="font-size: 12px;">Type a message below to begin chatting</p>
|
|
1508
|
+
`;
|
|
1509
|
+
messageListElement.appendChild(emptyState);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
messages.forEach((msg) => {
|
|
1513
|
+
// MessageWrapper - matches React UserMessageContainer
|
|
1514
|
+
const messageWrapper = document.createElement("div");
|
|
1515
|
+
messageWrapper.style.cssText = `
|
|
1516
|
+
display: flex;
|
|
1517
|
+
flex-direction: column;
|
|
1518
|
+
max-width: ${msg.isUser ? "70%" : "70%"};
|
|
1519
|
+
margin-left: ${msg.isUser ? "auto" : "0"};
|
|
1520
|
+
width: 100%;
|
|
1521
|
+
isolation: isolate;
|
|
1522
|
+
contain: layout style paint;
|
|
1523
|
+
`;
|
|
1524
|
+
const bubble = document.createElement("div");
|
|
1525
|
+
// Add data attribute to identify user messages for debugging/styling
|
|
1526
|
+
if (msg.isUser) {
|
|
1527
|
+
bubble.setAttribute("data-message-type", "user");
|
|
1528
|
+
}
|
|
1529
|
+
// Set base styles first - explicitly set background to match React UserBubble exactly
|
|
1530
|
+
// CRITICAL: Use the exact same background for both user and bot messages
|
|
1531
|
+
const backgroundColor = "rgba(252, 252, 253, 0.95)";
|
|
1532
|
+
bubble.style.cssText = `
|
|
1533
|
+
padding: 12px;
|
|
1534
|
+
border-radius: ${msg.isUser ? "12px 12px 4px 12px" : "12px 12px 12px 4px"};
|
|
1535
|
+
background: ${backgroundColor};
|
|
1536
|
+
background-color: ${backgroundColor};
|
|
1537
|
+
backdrop-filter: blur(20px) saturate(180%);
|
|
1538
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
1539
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8) inset, 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
1540
|
+
color: ${aeroTheme.colors.text.primary};
|
|
1541
|
+
font-family: ${aeroTheme.typography.fontFamily.body};
|
|
1542
|
+
font-size: 14px;
|
|
1543
|
+
line-height: 1.5;
|
|
1544
|
+
word-wrap: break-word;
|
|
1545
|
+
max-width: 100%;
|
|
1546
|
+
display: flex;
|
|
1547
|
+
flex-direction: column;
|
|
1548
|
+
font-synthesis: none;
|
|
1549
|
+
text-rendering: optimizeLegibility;
|
|
1550
|
+
-webkit-font-smoothing: antialiased;
|
|
1551
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1552
|
+
-webkit-text-size-adjust: 100%;
|
|
1553
|
+
isolation: isolate;
|
|
1554
|
+
contain: layout style paint;
|
|
1555
|
+
`;
|
|
1556
|
+
// Set background with !important to override any other styles (this is critical)
|
|
1557
|
+
bubble.style.setProperty("background", backgroundColor, "important");
|
|
1558
|
+
bubble.style.setProperty("background-color", backgroundColor, "important");
|
|
1559
|
+
// Header (Logo/Icon + Name) inside bubble - matches React MessageHeader
|
|
1560
|
+
if (msg.sender) {
|
|
1561
|
+
const header = document.createElement("div");
|
|
1562
|
+
header.style.cssText = `
|
|
1563
|
+
display: flex;
|
|
1564
|
+
align-items: center;
|
|
1565
|
+
gap: ${aeroTheme.spacing.xs};
|
|
1566
|
+
margin-bottom: ${aeroTheme.spacing.xs};
|
|
1567
|
+
isolation: isolate;
|
|
1568
|
+
contain: layout style paint;
|
|
1569
|
+
`;
|
|
1570
|
+
// Logo/Icon - matches React LogoWrapper
|
|
1571
|
+
const logoContainer = document.createElement("div");
|
|
1572
|
+
logoContainer.style.cssText = `
|
|
1573
|
+
width: 1rem;
|
|
1574
|
+
height: 1rem;
|
|
1575
|
+
display: flex;
|
|
1576
|
+
align-items: center;
|
|
1577
|
+
justify-content: center;
|
|
1578
|
+
isolation: isolate;
|
|
1579
|
+
contain: layout style paint;
|
|
1580
|
+
`;
|
|
1581
|
+
if (msg.isUser) {
|
|
1582
|
+
// User Icon - FaUser from react-icons/fa, matches React exactly
|
|
1583
|
+
const userIcon = Icons.User("sm");
|
|
1584
|
+
userIcon.style.width = "16px";
|
|
1585
|
+
userIcon.style.height = "16px";
|
|
1586
|
+
userIcon.style.color = aeroTheme.colors.convai.dark;
|
|
1587
|
+
logoContainer.appendChild(userIcon);
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
// Bot Logo - ConvaiLogo sm size with connected state
|
|
1591
|
+
const botLogo = Icons.ConvaiLogo("sm", "connected");
|
|
1592
|
+
botLogo.style.width = "16px";
|
|
1593
|
+
botLogo.style.height = "16px";
|
|
1594
|
+
botLogo.style.color = "#10b981";
|
|
1595
|
+
logoContainer.appendChild(botLogo);
|
|
1596
|
+
}
|
|
1597
|
+
// Name Label - matches React UserLabel exactly
|
|
1598
|
+
const nameLabel = document.createElement("span");
|
|
1599
|
+
nameLabel.textContent = msg.sender === "User" ? "You" : msg.sender;
|
|
1600
|
+
nameLabel.style.cssText = `
|
|
1601
|
+
font-size: 0.75rem;
|
|
1602
|
+
color: ${msg.isUser ? aeroTheme.colors.convai.dark : "#10b981"};
|
|
1603
|
+
font-weight: 500;
|
|
1604
|
+
font-family: ${aeroTheme.typography.fontFamily.body};
|
|
1605
|
+
isolation: isolate;
|
|
1606
|
+
contain: layout style paint;
|
|
1607
|
+
`;
|
|
1608
|
+
header.appendChild(logoContainer);
|
|
1609
|
+
header.appendChild(nameLabel);
|
|
1610
|
+
bubble.appendChild(header);
|
|
1611
|
+
}
|
|
1612
|
+
// Message Content
|
|
1613
|
+
const contentDiv = document.createElement("div");
|
|
1614
|
+
contentDiv.style.cssText = `
|
|
1615
|
+
white-space: pre-wrap;
|
|
1616
|
+
word-wrap: break-word;
|
|
1617
|
+
`;
|
|
1618
|
+
renderMarkdown(msg.text, contentDiv);
|
|
1619
|
+
bubble.appendChild(contentDiv);
|
|
1620
|
+
messageWrapper.appendChild(bubble);
|
|
1621
|
+
// Timestamp - matches React exactly
|
|
1622
|
+
if (msg.timestamp) {
|
|
1623
|
+
const timestamp = document.createElement("span");
|
|
1624
|
+
timestamp.textContent = msg.timestamp;
|
|
1625
|
+
timestamp.style.cssText = `
|
|
1626
|
+
font-size: 0.75rem;
|
|
1627
|
+
color: ${aeroTheme.colors.text.secondary};
|
|
1628
|
+
margin-top: 4px;
|
|
1629
|
+
align-self: ${msg.isUser ? "flex-end" : "flex-start"};
|
|
1630
|
+
font-family: ${aeroTheme.typography.fontFamily.body};
|
|
1631
|
+
`;
|
|
1632
|
+
messageWrapper.appendChild(timestamp);
|
|
1633
|
+
}
|
|
1634
|
+
messageListElement.appendChild(messageWrapper);
|
|
1635
|
+
});
|
|
1636
|
+
// Scroll to bottom
|
|
1637
|
+
messageListElement.scrollTop = messageListElement.scrollHeight;
|
|
1638
|
+
};
|
|
1639
|
+
const getBotStatusColor = () => {
|
|
1640
|
+
if (!convaiClient.state.isConnected) {
|
|
1641
|
+
return { color: "#ef4444", label: "Disconnected" };
|
|
1642
|
+
}
|
|
1643
|
+
if (convaiClient.state.isConnecting || !convaiClient.isBotReady) {
|
|
1644
|
+
return { color: "#f59e0b", label: "Connecting" };
|
|
1645
|
+
}
|
|
1646
|
+
return { color: "#10b981", label: "Connected" };
|
|
1647
|
+
};
|
|
1648
|
+
const formatMessages = () => {
|
|
1649
|
+
const filteredMessages = convaiClient.chatMessages.filter((msg) => (msg.type === "user-transcription" ||
|
|
1650
|
+
msg.type === "user-llm-text" ||
|
|
1651
|
+
msg.type === "bot-llm-text" ||
|
|
1652
|
+
msg.type === "bot-emotion") &&
|
|
1653
|
+
msg.content &&
|
|
1654
|
+
msg.content.trim().length > 0);
|
|
1655
|
+
return filteredMessages.map((msg) => ({
|
|
1656
|
+
id: msg.id,
|
|
1657
|
+
text: msg.content,
|
|
1658
|
+
isUser: msg.type.includes("user"),
|
|
1659
|
+
timestamp: new Date(msg.timestamp).toLocaleTimeString([], {
|
|
1660
|
+
hour: "2-digit",
|
|
1661
|
+
minute: "2-digit",
|
|
1662
|
+
}),
|
|
1663
|
+
sender: msg.type.includes("user") ? "You" : characterName,
|
|
1664
|
+
showLogo: msg.type.includes("bot"),
|
|
1665
|
+
}));
|
|
1666
|
+
};
|
|
1667
|
+
// Setup event listeners for client state changes
|
|
1668
|
+
const setupClientListeners = () => {
|
|
1669
|
+
convaiClient.on("stateChange", () => {
|
|
1670
|
+
updateHeader();
|
|
1671
|
+
updateMicButton();
|
|
1672
|
+
// Update pulse animation based on connecting state
|
|
1673
|
+
if (convaiClient.state.isConnecting) {
|
|
1674
|
+
morphingContainer.style.animation =
|
|
1675
|
+
"convai-pulse 2s ease-in-out infinite";
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
morphingContainer.style.animation = "none";
|
|
1679
|
+
}
|
|
1680
|
+
// Update speaking animation state
|
|
1681
|
+
if (convaiClient.state.isSpeaking) {
|
|
1682
|
+
if (!startTime) {
|
|
1683
|
+
startTime = Date.now();
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
// Reset when not speaking
|
|
1688
|
+
if (startTime && !convaiClient.state.isSpeaking) {
|
|
1689
|
+
startTime = 0;
|
|
1690
|
+
currentLevels = Array(40).fill(0.05);
|
|
1691
|
+
targetLevels = Array(40).fill(0.05);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
// Auto-collapse when disconnected
|
|
1695
|
+
if (!convaiClient.state.isConnected && !convaiClient.state.isConnecting) {
|
|
1696
|
+
if (isOpen) {
|
|
1697
|
+
setIsOpen(false);
|
|
1698
|
+
}
|
|
1699
|
+
isMuted = false;
|
|
1700
|
+
isVoiceMode = false;
|
|
1701
|
+
isVideoVisible = false;
|
|
1702
|
+
updateHeader();
|
|
1703
|
+
updateVoiceMode();
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
// Audio controls state change listener
|
|
1707
|
+
convaiClient.audioControls.on("audioStateChange", (audioState) => {
|
|
1708
|
+
if (audioState.isAudioMuted !== undefined) {
|
|
1709
|
+
updateMicButton();
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
// Video state change listener
|
|
1713
|
+
convaiClient.videoControls.on("videoStateChange", (videoState) => {
|
|
1714
|
+
if (videoState.isVideoEnabled !== undefined) {
|
|
1715
|
+
isVideoVisible = videoState.isVideoEnabled;
|
|
1716
|
+
updateVoiceMode();
|
|
1717
|
+
// Update tray button state
|
|
1718
|
+
const videoBtn = document.getElementById("convai-settings-video-btn");
|
|
1719
|
+
if (videoBtn) {
|
|
1720
|
+
const isActive = isVideoVisible;
|
|
1721
|
+
videoBtn.style.backgroundColor = isActive
|
|
1722
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
1723
|
+
: "transparent";
|
|
1724
|
+
videoBtn.style.color = isActive
|
|
1725
|
+
? "#10b981"
|
|
1726
|
+
: aeroTheme.colors.text.primary;
|
|
1727
|
+
// Update Icon - first child is icon container
|
|
1728
|
+
const iconContainer = videoBtn.querySelector("div");
|
|
1729
|
+
if (iconContainer) {
|
|
1730
|
+
iconContainer.innerHTML = "";
|
|
1731
|
+
const icon = isActive ? Icons.Video("md") : Icons.VideoOff("md");
|
|
1732
|
+
icon.style.width = "18px";
|
|
1733
|
+
icon.style.height = "18px";
|
|
1734
|
+
iconContainer.appendChild(icon);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
// Screen share state change listener
|
|
1740
|
+
convaiClient.screenShareControls.on("screenShareStateChange", (screenShareState) => {
|
|
1741
|
+
if (screenShareState.isScreenShareActive !== undefined) {
|
|
1742
|
+
const isSharing = screenShareState.isScreenShareActive;
|
|
1743
|
+
const shareBtn = document.getElementById("convai-settings-share-btn");
|
|
1744
|
+
if (shareBtn) {
|
|
1745
|
+
const isActive = isSharing;
|
|
1746
|
+
shareBtn.style.backgroundColor = isActive
|
|
1747
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
1748
|
+
: "transparent";
|
|
1749
|
+
shareBtn.style.color = isActive
|
|
1750
|
+
? "#10b981"
|
|
1751
|
+
: aeroTheme.colors.text.primary;
|
|
1752
|
+
// Update Icon - first child is icon container
|
|
1753
|
+
const iconContainer = shareBtn.querySelector("div");
|
|
1754
|
+
if (iconContainer) {
|
|
1755
|
+
iconContainer.innerHTML = "";
|
|
1756
|
+
const icon = isActive
|
|
1757
|
+
? Icons.StopScreenShare("md")
|
|
1758
|
+
: Icons.ScreenShare("md");
|
|
1759
|
+
icon.style.width = "18px";
|
|
1760
|
+
icon.style.height = "18px";
|
|
1761
|
+
iconContainer.appendChild(icon);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
convaiClient.on("messagesChange", () => {
|
|
1767
|
+
updateMessageList();
|
|
1768
|
+
});
|
|
1769
|
+
// Bot ready listener - updates UI when bot becomes ready
|
|
1770
|
+
convaiClient.on("botReady", () => {
|
|
1771
|
+
updateHeader(); // Update header to show green status
|
|
1772
|
+
});
|
|
1773
|
+
convaiClient.on("connect", () => {
|
|
1774
|
+
// Initialize audio renderer on connection
|
|
1775
|
+
if (convaiClient.room && !audioRenderer) {
|
|
1776
|
+
audioRenderer = new AudioRenderer(convaiClient.room);
|
|
1777
|
+
}
|
|
1778
|
+
// Fetch character info
|
|
1779
|
+
fetchCharacterInfo();
|
|
1780
|
+
});
|
|
1781
|
+
convaiClient.on("disconnect", () => {
|
|
1782
|
+
// Cleanup audio renderer
|
|
1783
|
+
if (audioRenderer) {
|
|
1784
|
+
audioRenderer.destroy();
|
|
1785
|
+
audioRenderer = null;
|
|
1786
|
+
}
|
|
1787
|
+
updateMessageList();
|
|
1788
|
+
});
|
|
1789
|
+
};
|
|
1790
|
+
// Initialize
|
|
1791
|
+
createDOM();
|
|
1792
|
+
setupClientListeners();
|
|
1793
|
+
// Set initial button state
|
|
1794
|
+
updateSendButton(); // Show voice mode button initially since input is empty
|
|
1795
|
+
// If already connected, initialize audio renderer and fetch character info
|
|
1796
|
+
if (convaiClient.state.isConnected && convaiClient.room) {
|
|
1797
|
+
audioRenderer = new AudioRenderer(convaiClient.room);
|
|
1798
|
+
fetchCharacterInfo();
|
|
1799
|
+
}
|
|
1800
|
+
// Return widget instance
|
|
1801
|
+
const widget = {
|
|
1802
|
+
element: rootElement,
|
|
1803
|
+
destroy: () => {
|
|
1804
|
+
// Cleanup audio renderer
|
|
1805
|
+
if (audioRenderer) {
|
|
1806
|
+
audioRenderer.destroy();
|
|
1807
|
+
audioRenderer = null;
|
|
1808
|
+
}
|
|
1809
|
+
// Remove event listeners
|
|
1810
|
+
morphingContainer.removeEventListener("click", handleToggle);
|
|
1811
|
+
// Remove DOM elements
|
|
1812
|
+
if (rootElement.parentElement) {
|
|
1813
|
+
rootElement.remove();
|
|
1814
|
+
}
|
|
1815
|
+
if (floatingVideo.parentElement) {
|
|
1816
|
+
floatingVideo.remove();
|
|
1817
|
+
}
|
|
1818
|
+
// Unsubscribe from client events
|
|
1819
|
+
convaiClient.off("stateChange", () => { });
|
|
1820
|
+
convaiClient.off("messagesChange", () => { });
|
|
1821
|
+
convaiClient.off("botReady", () => { });
|
|
1822
|
+
convaiClient.off("connect", () => { });
|
|
1823
|
+
convaiClient.off("disconnect", () => { });
|
|
1824
|
+
},
|
|
1825
|
+
};
|
|
1826
|
+
return widget;
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Destroy a Convai widget instance
|
|
1830
|
+
* @param widget - Widget instance to destroy
|
|
1831
|
+
*/
|
|
1832
|
+
export function destroyConvaiWidget(widget) {
|
|
1833
|
+
widget.destroy();
|
|
1834
|
+
}
|
|
1835
|
+
//# sourceMappingURL=ConvaiWidget.js.map
|