From 01d67eac4c2a4357aac80c25da6b9fb5247b74c0 Mon Sep 17 00:00:00 2001 From: Marc Klingen Date: Thu, 28 Nov 2024 18:28:40 +0100 Subject: [PATCH 1/2] feat: add inkeep --- components/GitHubBadge.tsx | 2 +- components/inkeep/InkeepChatButton.tsx | 41 + components/inkeep/InkeepCustomTrigger.tsx | 74 + components/inkeep/InkeepSearchBar.tsx | 40 + components/inkeep/useInkeepSettings.ts | 109 + components/logo.tsx | 4 +- components/supportChat/chat.tsx | 11 +- package.json | 1 + pages/_app.tsx | 2 + pnpm-lock.yaml | 3861 ++++++++++++++++++++- src/overrides.css | 12 +- theme.config.tsx | 5 +- 12 files changed, 3962 insertions(+), 200 deletions(-) create mode 100644 components/inkeep/InkeepChatButton.tsx create mode 100644 components/inkeep/InkeepCustomTrigger.tsx create mode 100644 components/inkeep/InkeepSearchBar.tsx create mode 100644 components/inkeep/useInkeepSettings.ts diff --git a/components/GitHubBadge.tsx b/components/GitHubBadge.tsx index bd60bfd47..32ecd5a2d 100644 --- a/components/GitHubBadge.tsx +++ b/components/GitHubBadge.tsx @@ -12,7 +12,7 @@ export const GithubMenuBadge = () => (
-
+
diff --git a/components/inkeep/InkeepChatButton.tsx b/components/inkeep/InkeepChatButton.tsx new file mode 100644 index 000000000..68a711210 --- /dev/null +++ b/components/inkeep/InkeepChatButton.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from "react"; +import useInkeepSettings from "./useInkeepSettings"; +import type { InkeepChatButtonProps } from "@inkeep/uikit"; +import { isChatOpen } from "../supportChat/chat"; + +export default function InkeepChatButton() { + const [ChatButton, setChatButton] = + useState<(e: InkeepChatButtonProps) => JSX.Element>(); + + const { baseSettings, aiChatSettings, searchSettings, modalSettings } = + useInkeepSettings(); + + // load the library asynchronously + useEffect(() => { + const loadChatButton = async () => { + try { + const { InkeepChatButton } = await import("@inkeep/uikit"); + setChatButton(() => InkeepChatButton); + } catch (error) { + console.error("Failed to load ChatButton:", error); + } + }; + + loadChatButton(); + }, []); + + const chatButtonProps: InkeepChatButtonProps = { + baseSettings, + aiChatSettings, + searchSettings, + modalSettings, + }; + + return ( + ChatButton && ( +
+ +
+ ) + ); +} diff --git a/components/inkeep/InkeepCustomTrigger.tsx b/components/inkeep/InkeepCustomTrigger.tsx new file mode 100644 index 000000000..46099cdc0 --- /dev/null +++ b/components/inkeep/InkeepCustomTrigger.tsx @@ -0,0 +1,74 @@ +import { useCallback, useEffect, useState } from "react"; +import useInkeepSettings from "./useInkeepSettings"; +import type { InkeepCustomTriggerProps } from "@inkeep/uikit"; +import { Search } from "lucide-react"; + +export default function InkeepCustomTrigger() { + const [isOpen, setIsOpen] = useState(false); + const [CustomTrigger, setCustomTrigger] = + useState<(e: InkeepCustomTriggerProps) => JSX.Element>(); + + const handleClose = useCallback(() => { + console.log("Modal closed"); + setIsOpen(false); + }, []); + + const { baseSettings, aiChatSettings, searchSettings, modalSettings } = + useInkeepSettings(); + + // Handle keyboard shortcuts + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + const isMac = navigator.platform.toLowerCase().includes("mac"); + const modifier = isMac ? event.metaKey : event.ctrlKey; + + if (modifier && event.key.toLowerCase() === "k") { + event.preventDefault(); + setIsOpen(true); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, []); + + // load the library asynchronously + useEffect(() => { + const loadCustomTrigger = async () => { + try { + const { InkeepCustomTrigger } = await import("@inkeep/uikit"); + setCustomTrigger(() => InkeepCustomTrigger); + } catch (error) { + console.error("Failed to load CustomTrigger:", error); + } + }; + + loadCustomTrigger(); + }, []); + + const customTriggerProps: InkeepCustomTriggerProps = { + isOpen, + onClose: handleClose, + baseSettings, + aiChatSettings, + searchSettings, + modalSettings, + }; + + return ( +
+
setIsOpen(true)} + className="relative flex items-center text-gray-900 dark:text-gray-300 contrast-more:text-gray-800 contrast-more:dark:text-gray-300 max-md:hidden hover:ring-2 hover:ring-gray-300 dark:hover:ring-gray-700 rounded-lg" + > +
+ Search or ask... +
+ + K + +
+ {CustomTrigger && } +
+ ); +} diff --git a/components/inkeep/InkeepSearchBar.tsx b/components/inkeep/InkeepSearchBar.tsx new file mode 100644 index 000000000..a4a88f5d7 --- /dev/null +++ b/components/inkeep/InkeepSearchBar.tsx @@ -0,0 +1,40 @@ +import React, { useEffect, useState } from "react"; +import useInkeepSettings from "./useInkeepSettings"; +import type { InkeepSearchBarProps } from "@inkeep/uikit"; + +export default function InkeepSearchBar() { + const [SearchBar, setSearchBar] = + useState<(e: InkeepSearchBarProps) => JSX.Element>(); + + const { baseSettings, aiChatSettings, searchSettings, modalSettings } = + useInkeepSettings(); + + // load the library asynchronously + useEffect(() => { + const loadSearchBar = async () => { + try { + const { InkeepSearchBar } = await import("@inkeep/uikit"); + setSearchBar(() => InkeepSearchBar); + } catch (error) { + console.error("Failed to load SearchBar:", error); + } + }; + + loadSearchBar(); + }, []); + + const searchBarProps: InkeepSearchBarProps = { + baseSettings, + aiChatSettings, + searchSettings, + modalSettings, + }; + + if (!SearchBar) return null; + + return ( +
+ +
+ ); +} diff --git a/components/inkeep/useInkeepSettings.ts b/components/inkeep/useInkeepSettings.ts new file mode 100644 index 000000000..c8f278b5d --- /dev/null +++ b/components/inkeep/useInkeepSettings.ts @@ -0,0 +1,109 @@ +import type { + InkeepAIChatSettings, + InkeepSearchSettings, + InkeepBaseSettings, + InkeepModalSettings, +} from "@inkeep/uikit"; +import { useTheme } from "nextra-theme-docs"; +import { openChat } from "../supportChat"; +import { MessageCircle } from "lucide-react"; + +type InkeepSharedSettings = { + baseSettings: InkeepBaseSettings; + aiChatSettings: InkeepAIChatSettings; + searchSettings: InkeepSearchSettings; + modalSettings: InkeepModalSettings; +}; + +const useInkeepSettings = (): InkeepSharedSettings => { + const { resolvedTheme } = useTheme(); + + const baseSettings: InkeepBaseSettings = { + apiKey: process.env.NEXT_PUBLIC_INKEEP_API_KEY!, + integrationId: process.env.NEXT_PUBLIC_INKEEP_INTEGRATION_ID!, + organizationId: process.env.NEXT_PUBLIC_INKEEP_ORGANIZATION_ID!, + primaryBrandColor: "#E11312", // your brand color, widget color scheme is derived from this + organizationDisplayName: "Langfuse", + // ...optional settings + colorMode: { + forcedColorMode: resolvedTheme, // to sync dark mode with the widget + }, + theme: { + components: { + SearchBarTrigger: { + defaultProps: { + size: "shrink", // 'expand' 'compact' 'shrink' 'medium' + variant: "subtle", // 'emphasized' 'subtle', + }, + }, + }, + }, + }; + + const modalSettings: InkeepModalSettings = { + // optional settings + }; + + const searchSettings: InkeepSearchSettings = { + placeholder: "Search...", + }; + + const aiChatSettings: InkeepAIChatSettings = { + // optional settings + chatSubjectName: "Langfuse", + botAvatarSrcUrl: "/icon256.png", // use your own bot avatar + // includeAIAnnotations: { + // shouldEscalateToSupport: true, + // }, + // aiAnnotationPolicies: { + // shouldEscalateToSupport: [ + // { + // threshold: "STANDARD", // "STRICT" or "STANDARD" + // action: { + // type: "SHOW_SUPPORT_BUTTON", + // label: "Contact Support", + // icon: { builtIn: "LuUsers" }, + // action: { + // type: "INVOKE_CALLBACK", + // callback: () => openChat(), + // }, + // }, + // }, + // ], + // }, + getHelpCallToActions: [ + { + name: "Chat with us", + type: "INVOKE_CALLBACK", + callback: () => openChat(), + icon: { + builtIn: "IoChatbubblesOutline", + }, + }, + { + name: "GitHub Support", + url: "https://langfuse.com/gh-support", + icon: { + builtIn: "FaGithub", + }, + }, + { + name: "Community Discord", + url: "https://langfuse.com/discord", + icon: { + builtIn: "FaDiscord", + }, + }, + ], + quickQuestions: [ + "How to use the Python decorator to trace my LLM app?", + "How to use Langfuse with Vercel AI SDK?", + "How to mask sensitive LLM data?", + "How to set up LLM-as-a-judge evals?", + ], + }; + + return { baseSettings, aiChatSettings, searchSettings, modalSettings }; +}; + +export default useInkeepSettings; diff --git a/components/logo.tsx b/components/logo.tsx index 88369da6c..78119ec12 100644 --- a/components/logo.tsx +++ b/components/logo.tsx @@ -22,14 +22,14 @@ export function Logo() { alt="Langfuse Logo" width={120} height={20} - className="hidden dark:block" + className="hidden dark:block max-w-28 sm:max-w-none" /> Langfuse Logo