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 ( <>