diff --git a/src/components/Pages/Homepage/DocsHeader/InlineSearch.tsx b/src/components/Pages/Homepage/DocsHeader/InlineSearch.tsx
index 8f183c78..18875b83 100644
--- a/src/components/Pages/Homepage/DocsHeader/InlineSearch.tsx
+++ b/src/components/Pages/Homepage/DocsHeader/InlineSearch.tsx
@@ -12,8 +12,6 @@ type InlineSearchProps = {
export function InlineSearch({ className = "", version }: InlineSearchProps) {
const {
- message,
- setMessage,
isOpen,
setIsOpen,
ModalSearchAndChat,
@@ -46,8 +44,6 @@ export function InlineSearch({ className = "", version }: InlineSearchProps) {
className={styles.searchInput}
onClick={() => setIsOpen(true)}
onFocus={() => setIsOpen(true)}
- value={message}
- onChange={(e) => setMessage(e.target.value)}
readOnly
/>
{ModalSearchAndChat && (
diff --git a/src/components/Search/InkeepSearch.tsx b/src/components/Search/InkeepSearch.tsx
index 5cc6ee6b..a9fe942e 100644
--- a/src/components/Search/InkeepSearch.tsx
+++ b/src/components/Search/InkeepSearch.tsx
@@ -6,13 +6,10 @@ import InkeepSearchIconSvg from "./inkeepIcon.svg";
export function InkeepSearch() {
const {
- message,
- setMessage,
isOpen,
setIsOpen,
ModalSearchAndChat,
inkeepModalProps,
- handleChange,
} = useInkeepSearch({
enableAIChat: true,
autoOpenOnInput: true,
@@ -25,10 +22,8 @@ export function InkeepSearch() {
handleChange(e.target.value)}
onClick={() => setIsOpen(true)}
placeholder="Search Docs"
- value={message}
/>
}>
diff --git a/src/hooks/useInkeepSearch.ts b/src/hooks/useInkeepSearch.ts
index f7667ce8..695f4152 100644
--- a/src/hooks/useInkeepSearch.ts
+++ b/src/hooks/useInkeepSearch.ts
@@ -1,5 +1,5 @@
-import { useState, useRef, useCallback, useEffect } from 'react';
-import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import { useState, useRef, useCallback, useEffect } from "react";
+import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import type {
InkeepAIChatSettings,
InkeepSearchSettings,
@@ -7,7 +7,10 @@ import type {
InkeepBaseSettings,
AIChatFunctions,
SearchFunctions,
-} from '@inkeep/cxkit-react';
+ InkeepCallbackEvent,
+ ConversationMessage,
+} from "@inkeep/cxkit-react";
+import { trackEvent } from "../utils/analytics";
interface UseInkeepSearchOptions {
version?: string;
@@ -18,15 +21,15 @@ interface UseInkeepSearchOptions {
}
export function useInkeepSearch(options: UseInkeepSearchOptions = {}) {
- const {
- version,
- enableKeyboardShortcut = false,
- keyboardShortcut = 'k',
+ const {
+ version,
+ enableKeyboardShortcut = false,
+ keyboardShortcut = "k",
enableAIChat = false,
autoOpenOnInput = false,
} = options;
-
- const [message, setMessage] = useState('');
+
+ const [message, setMessage] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [ModalSearchAndChat, setModalSearchAndChat] = useState(null);
@@ -39,7 +42,7 @@ export function useInkeepSearch(options: UseInkeepSearchOptions = {}) {
// Load the modal component dynamically
useEffect(() => {
(async () => {
- const { InkeepModalSearchAndChat } = await import('@inkeep/cxkit-react');
+ const { InkeepModalSearchAndChat } = await import("@inkeep/cxkit-react");
setModalSearchAndChat(() => InkeepModalSearchAndChat);
})();
}, []);
@@ -55,70 +58,206 @@ export function useInkeepSearch(options: UseInkeepSearchOptions = {}) {
}
};
- document.addEventListener('keydown', handleKeyDown);
- return () => document.removeEventListener('keydown', handleKeyDown);
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
}, [enableKeyboardShortcut, keyboardShortcut]);
const inkeepBaseSettings: InkeepBaseSettings = {
- apiKey: inkeepConfig.apiKey || '',
- organizationDisplayName: 'Teleport',
- primaryBrandColor: '#512FC9',
- aiApiBaseUrl: 'https://goteleport.com/inkeep-proxy',
- analyticsApiBaseUrl: 'https://goteleport.com/inkeep-proxy/analytics',
+ apiKey: inkeepConfig.apiKey || "",
+ organizationDisplayName: "Teleport",
+ primaryBrandColor: "#512FC9",
+ aiApiBaseUrl: "https://goteleport.com/inkeep-proxy",
+ analyticsApiBaseUrl: "https://goteleport.com/inkeep-proxy/analytics",
privacyPreferences: {
optOutAllAnalytics: false,
},
transformSource: (source) => {
const isDocs =
- source.contentType === 'docs' ||
- source.type === 'documentation';
+ source.contentType === "docs" || source.type === "documentation";
if (!isDocs) {
return source;
}
return {
...source,
- tabs: ['Docs', ...(source.tabs ?? [])],
- icon: { builtIn: 'IoDocumentTextOutline' },
+ tabs: ["Docs", ...(source.tabs ?? [])],
+ icon: { builtIn: "IoDocumentTextOutline" },
};
},
colorMode: {
- forcedColorMode: 'light',
+ forcedColorMode: "light",
},
theme: {
zIndex: {
- overlay: '2100',
- modal: '2200',
- popover: '2300',
- skipLink: '2400',
- toast: '2500',
- tooltip: '2600',
+ overlay: "2100",
+ modal: "2200",
+ popover: "2300",
+ skipLink: "2400",
+ toast: "2500",
+ tooltip: "2600",
},
},
+ // reference: https://docs.inkeep.com/cloud/ui-components/customization-guides/use-your-own-analytics
+ onEvent: (event: InkeepCallbackEvent) => {
+ const { eventName, properties } = event;
+
+ const eventsToTrack = [
+ "user_message_submitted",
+ "search_query_response_received",
+ "search_result_clicked",
+ "assistant_source_item_clicked",
+ "assistant_negative_feedback_submitted",
+ "assistant_positive_feedback_submitted",
+ "assistant_message_inline_link_opened",
+ "assistant_message_copied",
+ "assistant_code_block_copied",
+ ];
+
+ if (!eventsToTrack.includes(eventName)) {
+ return;
+ }
+
+ const getLatestMessage = (
+ messages: ConversationMessage[],
+ role: "assistant" | "system" | "user"
+ ) => {
+ return (
+ messages
+ .filter((msg) => msg.role === role)
+ .slice(-1)[0]
+ ?.content.slice(0, 100) || ""
+ );
+ };
+
+ try {
+ switch (eventName) {
+ case "search_query_response_received": {
+ if (properties.totalResults) {
+ trackEvent({
+ event_name: "search",
+ custom_parameters: {
+ search_term: properties.searchQuery.slice(0, 100),
+ total_results: properties.totalResults,
+ },
+ });
+ }
+ break;
+ }
+ case "user_message_submitted": {
+ trackEvent({
+ event_name: `inkeep_${eventName}`,
+ custom_parameters: {
+ latest_user_message: getLatestMessage(
+ properties.conversation.messages,
+ "user"
+ ),
+ },
+ });
+ break;
+ }
+ case "search_result_clicked": {
+ trackEvent({
+ event_name: `inkeep_${eventName}`,
+ custom_parameters: {
+ search_term: properties.searchQuery.slice(0, 100),
+ clicked_link_url: properties.url,
+ },
+ });
+ break;
+ }
+ case "assistant_source_item_clicked": {
+ trackEvent({
+ event_name: `inkeep_${eventName}`,
+ custom_parameters: {
+ latest_user_message: getLatestMessage(
+ properties.conversation.messages,
+ "user"
+ ),
+ clicked_link_url: properties.link.url,
+ },
+ });
+ break;
+ }
+ case "assistant_positive_feedback_submitted":
+ case "assistant_negative_feedback_submitted": {
+ trackEvent({
+ event_name: `inkeep_${eventName}`,
+ custom_parameters: {
+ latest_assistant_message: getLatestMessage(
+ properties.conversation.messages,
+ "assistant"
+ ),
+ feedback_reason_labels:
+ properties?.reasons?.map((r) => r.label).join(", ") || "",
+ feedback_reason_details:
+ properties?.reasons
+ ?.map((r) => r.details.slice(0, 100))
+ .join(", ") || "",
+ },
+ });
+ break;
+ }
+ case "assistant_message_inline_link_opened": {
+ trackEvent({
+ event_name: `inkeep_${eventName}`,
+ custom_parameters: {
+ clicked_link_url: properties.url,
+ },
+ });
+ break;
+ }
+ case "assistant_message_copied": {
+ trackEvent({
+ event_name: `inkeep_${eventName}`,
+ custom_parameters: {
+ latest_assistant_message: getLatestMessage(
+ properties.conversation.messages,
+ "assistant"
+ ),
+ },
+ });
+ break;
+ }
+ case "assistant_code_block_copied": {
+ trackEvent({
+ event_name: `inkeep_${eventName}`,
+ custom_parameters: {
+ code_value: properties.code.slice(0, 100),
+ code_language: properties.language || "",
+ },
+ });
+ break;
+ }
+ default:
+ break;
+ }
+ } catch (error) {
+ console.error("Error processing Inkeep event:", error);
+ }
+ },
};
const inkeepSearchSettings: InkeepSearchSettings = {
- placeholder: 'Search Docs',
+ placeholder: "Search Docs",
tabs: [
- ['Docs', { isAlwaysVisible: true }],
- ['GitHub', { isAlwaysVisible: true }],
+ ["Docs", { isAlwaysVisible: true }],
+ ["GitHub", { isAlwaysVisible: true }],
],
shouldOpenLinksInNewTab: true,
- view: 'dual-pane',
+ view: "dual-pane",
};
const inkeepAIChatSettings: InkeepAIChatSettings | undefined = enableAIChat
? {
- aiAssistantName: 'Teleport',
- aiAssistantAvatar: 'https://goteleport.com/static/pam-standing.svg',
+ aiAssistantName: "Teleport",
+ aiAssistantAvatar: "https://goteleport.com/static/pam-standing.svg",
}
: undefined;
const chatCallableFunctionsRef = useRef(null);
const searchCallableFunctionsRef = useRef(null);
- const handleChange = useCallback(
+ const handleSearchChange = useCallback(
(str: string) => {
- chatCallableFunctionsRef.current?.updateInputMessage(str);
searchCallableFunctionsRef.current?.updateQuery(str);
setMessage(str);
if (autoOpenOnInput && str) {
@@ -128,11 +267,22 @@ export function useInkeepSearch(options: UseInkeepSearchOptions = {}) {
[autoOpenOnInput]
);
+ const handleChatChange = useCallback(
+ (str: string) => {
+ chatCallableFunctionsRef.current?.updateInputMessage(str);
+ setMessage(str);
+ if (autoOpenOnInput && str) {
+ setIsOpen(true);
+ }
+ },
+ [autoOpenOnInput]
+ );
+
// Create dynamic search settings based on version
const dynamicSearchSettings = {
...inkeepSearchSettings,
searchFunctionsRef: searchCallableFunctionsRef,
- onQueryChange: handleChange,
+ onQueryChange: handleSearchChange,
// Add version-specific metadata if version is provided
...(version && {
metadata: {
@@ -152,22 +302,20 @@ export function useInkeepSearch(options: UseInkeepSearchOptions = {}) {
},
searchSettings: dynamicSearchSettings,
modalSettings: modalSettings,
- ...(enableAIChat && inkeepAIChatSettings && {
- aiChatSettings: {
- ...inkeepAIChatSettings,
- chatFunctionsRef: chatCallableFunctionsRef,
- onInputMessageChange: handleChange,
- },
- }),
+ ...(enableAIChat &&
+ inkeepAIChatSettings && {
+ aiChatSettings: {
+ ...inkeepAIChatSettings,
+ chatFunctionsRef: chatCallableFunctionsRef,
+ onInputMessageChange: handleChatChange,
+ },
+ }),
};
return {
- message,
- setMessage,
isOpen,
setIsOpen,
ModalSearchAndChat,
inkeepModalProps,
- handleChange,
};
-}
\ No newline at end of file
+}
diff --git a/src/theme/DocRoot/Layout/Main/index.tsx b/src/theme/DocRoot/Layout/Main/index.tsx
index 10f54e35..1bc1a23b 100644
--- a/src/theme/DocRoot/Layout/Main/index.tsx
+++ b/src/theme/DocRoot/Layout/Main/index.tsx
@@ -1,15 +1,107 @@
-// Manually swizzled to allow theming
+// Manually swizzled to allow theming
// See https://docusaurus.io/docs/swizzling
// This is wrapped, not ejected
-import React, {type ReactNode} from 'react';
-import Main from '@theme-original/DocRoot/Layout/Main';
-import type MainType from '@theme/DocRoot/Layout/Main';
-import type {WrapperProps} from '@docusaurus/types';
-import './styles.module.css';
+import React, { useEffect, type ReactNode } from "react";
+import Main from "@theme-original/DocRoot/Layout/Main";
+import type MainType from "@theme/DocRoot/Layout/Main";
+import type { WrapperProps } from "@docusaurus/types";
+import "./styles.module.css";
+import { trackEvent } from "@site/src/utils/analytics";
type Props = WrapperProps;
export default function MainWrapper(props: Props): ReactNode {
+ useEffect(() => {
+ const inkeepLinkTracker = (clickEvent: MouseEvent) => {
+ clickEvent.stopPropagation();
+
+ const path = clickEvent.composedPath();
+
+ if (!path) return;
+
+ const link = path.find((el) => el instanceof HTMLAnchorElement) as
+ | HTMLAnchorElement
+ | undefined;
+
+ const navbar = path.find(
+ (el) =>
+ el instanceof HTMLElement &&
+ el.classList?.contains("theme-layout-navbar")
+ ) as HTMLElement | undefined;
+
+ const sidebar = path.find(
+ (el) =>
+ el instanceof HTMLElement &&
+ el.classList?.contains("theme-doc-sidebar-menu")
+ ) as HTMLElement | undefined;
+
+ const mainContent = path.find(
+ (el) =>
+ el instanceof HTMLElement &&
+ el.classList?.contains("theme-doc-markdown")
+ ) as HTMLElement | undefined;
+
+ const breadcumbs = path.find(
+ (el) =>
+ el instanceof HTMLElement &&
+ el.classList?.contains("theme-doc-breadcrumbs")
+ ) as HTMLElement | undefined;
+
+ const toc = path.find(
+ (el) =>
+ el instanceof HTMLElement &&
+ (el.classList?.contains("theme-doc-toc-mobile") ||
+ el.classList?.contains("theme-doc-toc-desktop"))
+ ) as HTMLElement | undefined;
+
+ if (link && navbar) {
+ trackEvent({
+ event_name: "navbar_link_click",
+ custom_parameters: {
+ clicked_link_url: link.href,
+ },
+ });
+ }
+
+ if (link && sidebar) {
+ trackEvent({
+ event_name: "sidebar_link_click",
+ custom_parameters: {
+ clicked_link_url: link.href,
+ },
+ });
+ }
+
+ if (link && mainContent) {
+ trackEvent({
+ event_name: "active_page_link_click",
+ custom_parameters: {
+ clicked_link_url: link.href,
+ },
+ });
+ }
+
+ if (link && breadcumbs) {
+ trackEvent({
+ event_name: "breadcrumbs_link_click",
+ custom_parameters: {
+ clicked_link_url: link.href,
+ },
+ });
+ }
+
+ if (link && toc) {
+ trackEvent({
+ event_name: "toc_link_click",
+ });
+ }
+ };
+
+ window.addEventListener("click", inkeepLinkTracker);
+
+ return () => window.removeEventListener("click", inkeepLinkTracker);
+ }, []);
+
return (
<>