Skip to content

Commit a34bf7b

Browse files
committed
fix(askai): resolve TypeScript linting errors and make email optional
- Add ESLint exception for KapaFunction type parameters - Make email parameter optional in setUser and Window interface - Fix max-len violations by splitting long lines - Allow undefined values in ChatAttributes index type - Add proper type narrowing for dataset property access
1 parent 464a9bd commit a34bf7b

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed

assets/js/ask-ai.ts

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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

Comments
 (0)