|
| 1 | +import { productData, version } from './page-context.js'; |
| 2 | + |
| 3 | +// Type definitions for Kapa.ai widget |
| 4 | +declare global { |
| 5 | + interface Window { |
| 6 | + Kapa: KapaFunction; |
| 7 | + influxdatadocs: { |
| 8 | + AskAI: typeof AskAI; |
| 9 | + }; |
| 10 | + kapaSettings?: { |
| 11 | + user: { |
| 12 | + uniqueClientId: string; |
| 13 | + email?: string; |
| 14 | + }; |
| 15 | + }; |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | +// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars |
| 20 | +type KapaFunction = (command: string, options?: unknown) => void; |
| 21 | + |
| 22 | +interface ChatAttributes extends Record<string, string | undefined> { |
| 23 | + modalExampleQuestions?: string; |
| 24 | + sourceGroupIdsInclude?: string; |
| 25 | +} |
| 26 | + |
| 27 | +interface InitializeChatParams { |
| 28 | + onChatLoad: () => void; |
| 29 | + chatAttributes: ChatAttributes; |
| 30 | +} |
| 31 | + |
| 32 | +interface AskAIParams { |
| 33 | + userid?: string; |
| 34 | + email?: string; |
| 35 | + onChatLoad?: () => void; |
| 36 | + [key: string]: unknown; |
| 37 | +} |
| 38 | + |
| 39 | +function setUser(userid: string, email?: string): void { |
| 40 | + const NAMESPACE = 'kapaSettings'; |
| 41 | + |
| 42 | + // Set the user ID and email in the global settings namespace. |
| 43 | + // The chat widget will use this on subsequent chats to personalize |
| 44 | + // the user's experience. |
| 45 | + window[NAMESPACE] = { |
| 46 | + user: { |
| 47 | + uniqueClientId: userid, |
| 48 | + ...(email && { email }), |
| 49 | + }, |
| 50 | + }; |
| 51 | +} |
| 52 | + |
| 53 | +// Initialize the chat widget |
| 54 | +function initializeChat({ |
| 55 | + onChatLoad, |
| 56 | + chatAttributes, |
| 57 | +}: InitializeChatParams): void { |
| 58 | + /* See https://docs.kapa.ai/integrations/website-widget/configuration for |
| 59 | + * available configuration options. |
| 60 | + * All values are strings. |
| 61 | + */ |
| 62 | + // If you make changes to data attributes here, you also need to |
| 63 | + // port the changes to the api-docs/template.hbs API reference template. |
| 64 | + const requiredAttributes = { |
| 65 | + websiteId: 'a02bca75-1dd3-411e-95c0-79ee1139be4d', |
| 66 | + projectName: 'InfluxDB', |
| 67 | + projectColor: '#020a47', |
| 68 | + projectLogo: '/img/influx-logo-cubo-white.png', |
| 69 | + }; |
| 70 | + |
| 71 | + const optionalAttributes = { |
| 72 | + modalDisclaimer: |
| 73 | + 'This AI can access [documentation for InfluxDB, clients, and related tools](https://docs.influxdata.com). Information you submit is used in accordance with our [Privacy Policy](https://www.influxdata.com/legal/privacy-policy/).', |
| 74 | + modalExampleQuestions: |
| 75 | + 'Use Python to write data to InfluxDB 3,How do I query using SQL?,How do I use MQTT with Telegraf?', |
| 76 | + buttonHide: 'true', |
| 77 | + exampleQuestionButtonWidth: 'auto', |
| 78 | + modalOpenOnCommandK: 'true', |
| 79 | + modalExampleQuestionsColSpan: '8', |
| 80 | + modalFullScreenOnMobile: 'true', |
| 81 | + modalHeaderPadding: '.5rem', |
| 82 | + modalInnerPositionRight: '0', |
| 83 | + modalInnerPositionLeft: '', |
| 84 | + modalLockScroll: 'false', |
| 85 | + modalOverrideOpenClassAskAi: 'ask-ai-open', |
| 86 | + modalSize: '640px', |
| 87 | + modalWithOverlay: 'false', |
| 88 | + modalYOffset: '10vh', |
| 89 | + userAnalyticsFingerprintEnabled: 'true', |
| 90 | + fontFamily: 'Proxima Nova, sans-serif', |
| 91 | + modalHeaderBgColor: 'linear-gradient(90deg, #d30971 0%, #9b2aff 100%)', |
| 92 | + modalHeaderBorderBottom: 'none', |
| 93 | + modalTitleColor: '#fff', |
| 94 | + modalTitleFontSize: '1.25rem', |
| 95 | + }; |
| 96 | + |
| 97 | + const scriptUrl = 'https://widget.kapa.ai/kapa-widget.bundle.js'; |
| 98 | + const script = document.createElement('script'); |
| 99 | + script.async = true; |
| 100 | + script.src = scriptUrl; |
| 101 | + script.onload = function () { |
| 102 | + onChatLoad(); |
| 103 | + window.influxdatadocs.AskAI = AskAI; |
| 104 | + }; |
| 105 | + script.onerror = function () { |
| 106 | + console.error('Error loading AI chat widget script'); |
| 107 | + }; |
| 108 | + |
| 109 | + const dataset = { |
| 110 | + ...requiredAttributes, |
| 111 | + ...optionalAttributes, |
| 112 | + ...chatAttributes, |
| 113 | + }; |
| 114 | + Object.keys(dataset).forEach((key) => { |
| 115 | + // Assign dataset attributes from the object |
| 116 | + const value = dataset[key as keyof typeof dataset]; |
| 117 | + if (value !== undefined) { |
| 118 | + script.dataset[key] = value; |
| 119 | + } |
| 120 | + }); |
| 121 | + |
| 122 | + // Check for an existing script element to remove |
| 123 | + const oldScript = document.querySelector(`script[src="${scriptUrl}"]`); |
| 124 | + if (oldScript) { |
| 125 | + oldScript.remove(); |
| 126 | + } |
| 127 | + document.head.appendChild(script); |
| 128 | +} |
| 129 | + |
| 130 | +function getVersionSpecificConfig(configKey: string): unknown { |
| 131 | + // Try version-specific config first (e.g., ai_sample_questions__v1) |
| 132 | + if (version && version !== 'n/a') { |
| 133 | + const versionKey = `${configKey}__v${version}`; |
| 134 | + const versionConfig = productData?.product?.[versionKey]; |
| 135 | + if (versionConfig) { |
| 136 | + return versionConfig; |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + // Fall back to default config |
| 141 | + return productData?.product?.[configKey]; |
| 142 | +} |
| 143 | + |
| 144 | +function getProductExampleQuestions(): string { |
| 145 | + const questions = getVersionSpecificConfig('ai_sample_questions') as |
| 146 | + | string[] |
| 147 | + | undefined; |
| 148 | + if (!questions || questions.length === 0) { |
| 149 | + return ''; |
| 150 | + } |
| 151 | + |
| 152 | + // Only add version hints for InfluxDB database products |
| 153 | + // Other tools like Explorer, Telegraf, Chronograf, Kapacitor, |
| 154 | + // Flux don't need version hints |
| 155 | + const productNamespace = productData?.product?.namespace; |
| 156 | + const shouldAddVersionHint = |
| 157 | + productNamespace === 'influxdb' || |
| 158 | + productNamespace === 'influxdb3' || |
| 159 | + productNamespace === 'enterprise_influxdb'; |
| 160 | + |
| 161 | + if (!shouldAddVersionHint) { |
| 162 | + return questions.join(','); |
| 163 | + } |
| 164 | + |
| 165 | + // Extract version subpath for hint |
| 166 | + const pathParts = window.location.pathname.split('/').filter(Boolean); |
| 167 | + const versionPath = |
| 168 | + pathParts.length >= 2 |
| 169 | + ? `/${pathParts[0]}/${pathParts[1]}/` |
| 170 | + : window.location.pathname; |
| 171 | + |
| 172 | + // Append version hint to each question |
| 173 | + const questionsWithHint = questions.map((question) => { |
| 174 | + return `${question} (Version: ${versionPath})`; |
| 175 | + }); |
| 176 | + |
| 177 | + return questionsWithHint.join(','); |
| 178 | +} |
| 179 | + |
| 180 | +function getProductSourceGroupIds(): string { |
| 181 | + const sourceGroupIds = getVersionSpecificConfig('ai_source_group_ids') as |
| 182 | + | string |
| 183 | + | undefined; |
| 184 | + return sourceGroupIds || ''; |
| 185 | +} |
| 186 | + |
| 187 | +function getVersionContext(): string { |
| 188 | + // Only add version context for InfluxDB database products |
| 189 | + const productNamespace = productData?.product?.namespace; |
| 190 | + const shouldAddVersionContext = |
| 191 | + productNamespace === 'influxdb' || |
| 192 | + productNamespace === 'influxdb3' || |
| 193 | + productNamespace === 'enterprise_influxdb'; |
| 194 | + |
| 195 | + if (!shouldAddVersionContext) { |
| 196 | + return ''; |
| 197 | + } |
| 198 | + |
| 199 | + // Extract version subpath for context |
| 200 | + const pathParts = window.location.pathname.split('/').filter(Boolean); |
| 201 | + const versionPath = |
| 202 | + pathParts.length >= 2 |
| 203 | + ? `/${pathParts[0]}/${pathParts[1]}/` |
| 204 | + : window.location.pathname; |
| 205 | + |
| 206 | + return `(Version: ${versionPath})`; |
| 207 | +} |
| 208 | + |
| 209 | +function setupVersionPrefill(): void { |
| 210 | + const versionContext = getVersionContext(); |
| 211 | + if (!versionContext) { |
| 212 | + console.log('[AskAI] No version context needed'); |
| 213 | + return; |
| 214 | + } |
| 215 | + |
| 216 | + console.log('[AskAI] Version context:', versionContext); |
| 217 | + |
| 218 | + // Wait for Kapa to be available |
| 219 | + const checkKapa = (): void => { |
| 220 | + if (!window.Kapa || typeof window.Kapa !== 'function') { |
| 221 | + console.log('[AskAI] Waiting for Kapa...'); |
| 222 | + setTimeout(checkKapa, 100); |
| 223 | + return; |
| 224 | + } |
| 225 | + |
| 226 | + console.log('[AskAI] Kapa found (preinitialized)'); |
| 227 | + |
| 228 | + // Use Kapa event system to intercept modal opens |
| 229 | + window.Kapa('onModalOpen', () => { |
| 230 | + console.log('[AskAI] Modal opened'); |
| 231 | + |
| 232 | + // Wait a moment for the input to be rendered |
| 233 | + setTimeout(() => { |
| 234 | + // Find the textarea input |
| 235 | + const textarea = document.querySelector<HTMLTextAreaElement>( |
| 236 | + 'textarea[placeholder*="Ask"]' |
| 237 | + ); |
| 238 | + console.log('[AskAI] Textarea found:', !!textarea); |
| 239 | + |
| 240 | + if (textarea && (!textarea.value || textarea.value.trim() === '')) { |
| 241 | + console.log('[AskAI] Setting textarea value to:', versionContext); |
| 242 | + textarea.value = versionContext; |
| 243 | + |
| 244 | + // Dispatch input event to notify React |
| 245 | + const inputEvent = new Event('input', { bubbles: true }); |
| 246 | + textarea.dispatchEvent(inputEvent); |
| 247 | + |
| 248 | + // Focus at the beginning so user can start typing |
| 249 | + textarea.setSelectionRange(0, 0); |
| 250 | + textarea.focus(); |
| 251 | + |
| 252 | + console.log('[AskAI] Version context added to input'); |
| 253 | + } else { |
| 254 | + console.log('[AskAI] Textarea already has value or not found'); |
| 255 | + } |
| 256 | + }, 100); |
| 257 | + }); |
| 258 | + |
| 259 | + console.log('[AskAI] Version pre-fill setup complete'); |
| 260 | + }; |
| 261 | + |
| 262 | + checkKapa(); |
| 263 | +} |
| 264 | + |
| 265 | +/** |
| 266 | + * Initialize the Ask AI chat widget with version-aware source filtering |
| 267 | + * |
| 268 | + * @param params - Configuration parameters |
| 269 | + * @param params.userid - Optional unique user ID |
| 270 | + * @param params.email - Optional user email |
| 271 | + * @param params.onChatLoad - Optional callback when chat widget loads |
| 272 | + * @param params.chatParams - Additional Kapa widget configuration attributes |
| 273 | + */ |
| 274 | +export default function AskAI({ |
| 275 | + userid, |
| 276 | + email, |
| 277 | + onChatLoad, |
| 278 | + ...chatParams |
| 279 | +}: AskAIParams): void { |
| 280 | + const modalExampleQuestions = getProductExampleQuestions(); |
| 281 | + const sourceGroupIds = getProductSourceGroupIds(); |
| 282 | + const chatAttributes: ChatAttributes = { |
| 283 | + ...(modalExampleQuestions && { modalExampleQuestions }), |
| 284 | + ...(sourceGroupIds && { sourceGroupIdsInclude: sourceGroupIds }), |
| 285 | + ...(chatParams as Record<string, string>), |
| 286 | + }; |
| 287 | + |
| 288 | + const wrappedOnChatLoad = (): void => { |
| 289 | + // Setup version pre-fill after widget loads |
| 290 | + setupVersionPrefill(); |
| 291 | + // Call original onChatLoad if provided |
| 292 | + if (onChatLoad) { |
| 293 | + onChatLoad(); |
| 294 | + } |
| 295 | + }; |
| 296 | + |
| 297 | + initializeChat({ onChatLoad: wrappedOnChatLoad, chatAttributes }); |
| 298 | + |
| 299 | + if (userid) { |
| 300 | + setUser(userid, email); |
| 301 | + } |
| 302 | +} |
0 commit comments