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"
/>