diff --git a/CLAUDE.md b/CLAUDE.md
index df4bad9..cda9dd5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -115,7 +115,6 @@ function App() {
- `keyboardShortcut`: Keyboard shortcut to open search (default: "cmd+k")
- `buttonText`: Custom search button text
- `buttonProps`: Additional props for the search button
-- `baseAskaiUrl`: Custom AI chat API endpoint
## π― Key Components
diff --git a/apps/docs/content/docs/experiences/ask-ai/sidepanel-askai.mdx b/apps/docs/content/docs/experiences/ask-ai/sidepanel-askai.mdx
new file mode 100644
index 0000000..8807dea
--- /dev/null
+++ b/apps/docs/content/docs/experiences/ask-ai/sidepanel-askai.mdx
@@ -0,0 +1,153 @@
+---
+title: Sidepanel Ask AI
+description: "Sidepanel chat experience with Ask AI, powered by Algolia's Ask AI. Opens as a side panel that doesn't block the rest of the page."
+---
+
+import { File, Folder, Files } from 'fumadocs-ui/components/files';
+import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContents } from '@/components/animate-ui/components/animate/tabs';
+import { LogosShadcn } from '@/components/icons/shadcn';
+import { TypeTable } from 'fumadocs-ui/components/type-table';
+
+A sidepanel chat experience that opens from the right side of the screen, allowing users to interact with [Algolia's Ask AI](https://www.algolia.com/products/ai/ask-ai) without blocking the rest of the page. Perfect for documentation sites, help centers, or any context where users need quick AI assistance while browsing.
+
+## Use cases
+
+- Documentation site AI assistant
+- Help center chat support
+- Non-blocking AI chat interface
+- Side-by-side browsing and AI interaction
+- Keyboard-accessible chat experience
+
+## Preview
+
+
+
+
+
+
Usage
+
+ shadcn/ui
+
+
+
+
+```tsx
+npx shadcn@latest add @algolia/sidepanel-askai
+```
+
+This will add the `Sidepanel Ask AI` experience to your project under `components/sidepanel-askai`. Use it like this:
+
+```tsx
+import SidepanelExperience from "@/components/sidepanel-askai";
+
+
+```
+
+### Configuration
+
+Required props for using the `SidepanelExperience` component:
+
+- `applicationId`: The Algolia application ID
+- `apiKey`: The Algolia API key
+- `indexName`: The Algolia index name
+- `assistantId`: The Ask AI assistant ID
+
+### Structure
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Behavior
+
+- **Opens via button or keyboard**: Click the "Ask AI" button or press `Cmd+I` (Mac) / `Ctrl+I` (Windows)
+- **Non-blocking**: The rest of the page remains interactive while the sidepanel is open
+- **Resizable**: Toggle between 360px (default) and 580px (maximized) width on desktop
+- **Keyboard accessible**: Press `Escape` to close, `Enter` to send messages
+- **Auto-scroll**: Chat automatically scrolls to show new messages
+- **Mobile responsive**: Full-width on mobile, fixed width on desktop
+
+## References
+",
+ parameters: [
+ {
+ name: "onClick",
+ description: "The function to call when the button is clicked",
+ },
+ {
+ name: "children",
+ description: "The children to render in the button (optional)",
+ },
+ ],
+ required: false,
+ },
+ }}
+/>
+
+## Extending further
+
+- Change the styling to match your brand
+- Customize the sidepanel width and animations
+- Add custom message handlers
+- Integrate with your analytics system
+- Open in v0 for more customization
+
diff --git a/apps/docs/content/docs/experiences/search-askai.mdx b/apps/docs/content/docs/experiences/search-askai.mdx
index aac3552..b0e29fb 100644
--- a/apps/docs/content/docs/experiences/search-askai.mdx
+++ b/apps/docs/content/docs/experiences/search-askai.mdx
@@ -200,18 +200,18 @@ Required props for using the `SearchWithAskAi` component:
],
required: true,
},
- baseAskaiUrl: {
- description: "Base URL for AI chat API (optional, defaults to beta endpoint)",
- type: "string",
- default: "\"https://askai.algolia.com\"",
- required: false,
- },
placeholder: {
description: "Placeholder text for search input (optional, defaults to \"What are you looking for?\")",
type: "string",
default: "\"What are you looking for?\"",
required: false,
},
+ suggestedQuestionsEnabled: {
+ description: "Suggested Questions Enabled (optional, defaults to false)",
+ type: "boolean",
+ default: false,
+ required: false,
+ },
hitsPerPage: {
description: "Number of hits per page (optional, defaults to 8)",
type: "number",
diff --git a/apps/docs/content/docs/experiences/search.mdx b/apps/docs/content/docs/experiences/search.mdx
index 59824d5..1bcc135 100644
--- a/apps/docs/content/docs/experiences/search.mdx
+++ b/apps/docs/content/docs/experiences/search.mdx
@@ -179,12 +179,6 @@ import Search from "@/components/search";
],
required: true,
},
- baseAskaiUrl: {
- description: "Base URL for AI chat API (optional, defaults to beta endpoint)",
- type: "string",
- default: "\"https://askai.algolia.com\"",
- required: false,
- },
placeholder: {
description: "Placeholder text for search input (optional, defaults to \"What are you looking for?\")",
type: "string",
diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json
index bf5df08..a4562b8 100644
--- a/apps/docs/content/docs/meta.json
+++ b/apps/docs/content/docs/meta.json
@@ -11,6 +11,7 @@
"experiences/search-askai",
"experiences/dropdown-search",
"---Ask AI Experiences---",
+ "experiences/ask-ai/sidepanel-askai",
"experiences/ask-ai/highlight-to-askai"
]
}
diff --git a/apps/docs/public/r/dropdown-search.json b/apps/docs/public/r/dropdown-search.json
new file mode 100644
index 0000000..e8246c2
--- /dev/null
+++ b/apps/docs/public/r/dropdown-search.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "dropdown-search",
+ "type": "registry:block",
+ "title": "Algolia dropdown search experience",
+ "description": "Inline autocomplete dropdown search experience, powered by Algolia's InstantSearch",
+ "dependencies": [
+ "algoliasearch@^5",
+ "react-instantsearch@^7.16.2",
+ "lucide-react",
+ "@radix-ui/react-popover"
+ ],
+ "registryDependencies": [
+ "input"
+ ],
+ "files": [
+ {
+ "path": "src/registry/experiences/dropdown-search/components/dropdown-search.tsx",
+ "content": "/** biome-ignore-all lint/a11y/useSemanticElements: hand crafted interactions */\n/** biome-ignore-all lint/a11y/useFocusableInteractive: hand crafted interactions */\n/** biome-ignore-all lint/suspicious/noExplicitAny: too ambiguous */\n/** biome-ignore-all lint/a11y/noStaticElementInteractions: hand crafted interactions */\n/** biome-ignore-all lint/a11y/useKeyWithClickEvents: hand crafted interactions */\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport * as Popover from \"@radix-ui/react-popover\";\nimport { liteClient as algoliasearch } from \"algoliasearch/lite\";\nimport { SearchIcon } from \"lucide-react\";\nimport type React from \"react\";\nimport { memo, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n Configure,\n Highlight,\n InstantSearch,\n useHits,\n useSearchBox,\n} from \"react-instantsearch\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { useKeyboardNavigation } from \"@/registry/experiences/dropdown-search/hooks/use-keyboard-navigation\";\n\nexport interface DropdownSearchConfig {\n /** Algolia Application ID (required) */\n applicationId: string;\n /** Algolia API Key (required) */\n apiKey: string;\n /** Algolia Index Name (required) */\n indexName: string;\n /** Placeholder text for search input (optional, defaults to \"Search...\") */\n placeholder?: string;\n /** Number of hits per page (optional, defaults to 5) */\n hitsPerPage?: number;\n /** Map which hit attributes to render (supports dotted paths) */\n attributes?: HitsAttributesMapping;\n /** Custom className for the root container */\n className?: string;\n /** Max height for dropdown (optional, defaults to \"300px\") */\n maxHeight?: string;\n /** Enable Algolia Insights (optional, defaults to true) */\n insights?: boolean;\n /** Additional Algolia search parameters (optional) - e.g., analytics, filters, distinct, etc. */\n searchParameters?: Record;\n}\n\n// =========================================================================\n// Attribute Mapping\n// =========================================================================\n\ntype HitsAttributesMapping = {\n primaryText: string;\n secondaryText?: string;\n tertiaryText?: string;\n url?: string;\n image?: string;\n};\n\nfunction toAttributePath(attribute?: string): string | string[] | undefined {\n if (!attribute) return undefined;\n return attribute.includes(\".\") ? attribute.split(\".\") : attribute;\n}\n\nfunction getByPath(obj: unknown, path?: string): T | undefined {\n if (!obj || !path) return undefined;\n const parts = path.split(\".\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = (current as Record)[part];\n }\n return current as T | undefined;\n}\n\n// ============================================================================\n// Internal Components\n// ============================================================================\n\ninterface HitsListProps {\n hits: any[];\n query: string;\n selectedIndex: number;\n attributes?: HitsAttributesMapping;\n onItemClick?: () => void;\n onHoverIndex?: (index: number) => void;\n hoverEnabled?: boolean;\n sendEvent?: (eventType: \"click\", hit: any, eventName: string) => void;\n}\n\nconst HitsList = memo(function HitsList({\n hits,\n selectedIndex,\n attributes,\n onItemClick,\n onHoverIndex,\n hoverEnabled,\n sendEvent,\n}: HitsListProps) {\n const [failedImages, setFailedImages] = useState>({});\n const mapping = useMemo(\n () => ({\n primaryText: attributes?.primaryText,\n secondaryText: attributes?.secondaryText,\n tertiaryText: attributes?.tertiaryText,\n url: attributes?.url,\n image: attributes?.image,\n }),\n [attributes],\n );\n\n if (!attributes || !mapping.primaryText) {\n throw new Error(\"At least a primaryText is required to display results\");\n }\n\n return (\n <>\n {hits.map((hit: any, idx: number) => {\n const isSel = selectedIndex === idx;\n const imageUrl = getByPath(hit, mapping.image);\n const url = getByPath(hit, mapping.url);\n const hasImage = Boolean(imageUrl);\n const isImageFailed = failedImages[hit.objectID] || !hasImage;\n const primaryVal = getByPath(hit, mapping.primaryText);\n return (\n {\n sendEvent?.(\"click\", hit, \"Hit Clicked\");\n onItemClick?.();\n }}\n className=\"flex flex-row items-center gap-3 cursor-pointer text-decoration-none text-foreground bg-background rounded-sm p-3 aria-selected:bg-accent transition-colors\"\n role=\"option\"\n aria-selected={isSel}\n onMouseEnter={() => {\n if (!hoverEnabled) return;\n onHoverIndex?.(idx);\n }}\n onMouseMove={() => {\n if (!hoverEnabled) return;\n onHoverIndex?.(idx);\n }}\n >\n {hasImage ? (\n
\n {/* π§ DO NOT REMOVE the logo if you are on a Free plan\n * https://support.algolia.com/hc/en-us/articles/17226079853073-Is-displaying-the-Algolia-logo-required\n */}\n \n Powered by \n \n \n
\n {/* π§ DO NOT REMOVE the logo if you are on a Free plan\n * https://support.algolia.com/hc/en-us/articles/17226079853073-Is-displaying-the-Algolia-logo-required\n */}\n \n Powered by \n \n \n
\n {/* π§ DO NOT REMOVE the logo if you are on a Free plan\n * https://support.algolia.com/hc/en-us/articles/17226079853073-Is-displaying-the-Algolia-logo-required\n */}\n \n Powered by\n \n \n
\n
\n );\n});\n\nexport default function SearchExperience(config: SearchConfig) {\n const searchClient = algoliasearch(config.applicationId, config.apiKey);\n searchClient.addAlgoliaAgent(\"algolia-sitesearch\");\n\n const [isModalOpen, setIsModalOpen] = useState(false);\n\n const openModal = () => setIsModalOpen(true);\n const closeModal = () => setIsModalOpen(false);\n\n // Parse keyboard shortcut (defaults to cmd+k)\n const shortcut = config.keyboardShortcut || \"cmd+k\";\n const [modifierKey, key] = shortcut.toLowerCase().split(\"+\");\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: we don't to rerun\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n const isModifierPressed =\n modifierKey === \"cmd\"\n ? event.metaKey || event.ctrlKey\n : event.getModifierState(\n modifierKey.charAt(0).toUpperCase() + modifierKey.slice(1),\n );\n\n if (isModifierPressed && event.key.toLowerCase() === key) {\n event.preventDefault();\n openModal();\n }\n };\n\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => document.removeEventListener(\"keydown\", handleKeyDown);\n }, [modifierKey, key]);\n\n const buttonProps = {\n ...config.buttonProps,\n onClick: openModal,\n };\n\n return (\n <>\n {config.buttonText}\n \n \n \n \n \n >\n );\n}\n",
+ "content": "/** biome-ignore-all lint/a11y/useSemanticElements: hand crafted interactions */\n/** biome-ignore-all lint/a11y/useFocusableInteractive: hand crafted interactions */\n/** biome-ignore-all lint/suspicious/noExplicitAny: too ambiguous */\n/** biome-ignore-all lint/a11y/noStaticElementInteractions: hand crafted interactions */\n/** biome-ignore-all lint/a11y/useKeyWithClickEvents: hand crafted interactions */\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { liteClient as algoliasearch } from \"algoliasearch/lite\";\nimport { ArrowDown, ArrowUp, CornerDownLeft, SearchIcon } from \"lucide-react\";\nimport type React from \"react\";\nimport { memo, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n Configure,\n Highlight,\n InstantSearch,\n useHits,\n useInstantSearch,\n useSearchBox,\n} from \"react-instantsearch\";\nimport { Button } from \"@/components/ui/button\";\nimport { useKeyboardNavigation } from \"@/registry/experiences/search/hooks/use-keyboard-navigation\";\n\nexport interface SearchConfig {\n /** Algolia Application ID (required) */\n applicationId: string;\n /** Algolia API Key (required) */\n apiKey: string;\n /** Algolia Index Name (required) */\n indexName: string;\n /** Placeholder text for search input (optional, defaults to \"What are you looking for?\") */\n placeholder?: string;\n /** Number of hits per page (optional, defaults to 8) */\n hitsPerPage?: number;\n /** Keyboard shortcut to open search (optional, defaults to \"cmd+k\") */\n keyboardShortcut?: string;\n /** Custom search button text (optional) */\n buttonText?: string;\n /** Custom search button props (optional) */\n buttonProps?: React.ComponentProps;\n /** Map which hit attributes to render (supports dotted paths) */\n attributes: HitsAttributesMapping;\n /** Additional Algolia search parameters (optional) - e.g., analytics, filters, distinct, etc. */\n searchParameters?: Record;\n /** Enable Algolia Insights (optional, defaults to true) */\n insights?: boolean;\n}\n// =========================================================================\n// Attribute Mapping\n// =========================================================================\n\ntype HitsAttributesMapping = {\n primaryText: string;\n secondaryText?: string;\n tertiaryText?: string;\n url?: string;\n image?: string;\n};\n\nfunction toAttributePath(attribute?: string): string | string[] | undefined {\n if (!attribute) return undefined;\n return attribute.includes(\".\") ? attribute.split(\".\") : attribute;\n}\n\nfunction getByPath(obj: unknown, path?: string): T | undefined {\n if (!obj || !path) return undefined;\n const parts = path.split(\".\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = (current as Record)[part];\n }\n return current as T | undefined;\n}\n\n// ============================================================================\n// Internal Components\n// ============================================================================\n\ninterface SearchButtonProps {\n onClick: () => void;\n children?: React.ReactNode;\n}\n\nexport const SearchButton: React.FC = ({ onClick }) => {\n return (\n \n );\n};\n\n// Logo Component\nconst AlgoliaLogo = ({ size = 150 }: { size?: number | string }) => (\n \n);\n\n// Modal Component\ninterface ModalProps {\n isOpen: boolean;\n onClose: () => void;\n children: React.ReactNode;\n}\n\nconst Modal: React.FC = ({ isOpen, onClose, children }) => {\n useEffect(() => {\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n onClose();\n }\n };\n\n if (isOpen) {\n document.addEventListener(\"keydown\", handleEscape);\n document.body.style.overflow = \"hidden\";\n }\n\n return () => {\n document.removeEventListener(\"keydown\", handleEscape);\n document.body.style.overflow = \"unset\";\n };\n }, [isOpen, onClose]);\n\n if (!isOpen) return null;\n\n return createPortal(\n
\n {/* π§ DO NOT REMOVE the logo if you are on a Free plan\n * https://support.algolia.com/hc/en-us/articles/17226079853073-Is-displaying-the-Algolia-logo-required\n */}\n \n Powered by\n \n \n