diff --git a/examples/demo-askai/src/App.tsx b/examples/demo-askai/src/App.tsx index 2cfa933f..bcc1da74 100644 --- a/examples/demo-askai/src/App.tsx +++ b/examples/demo-askai/src/App.tsx @@ -12,8 +12,7 @@ function App(): JSX.Element { indexName="beta-react" appId="betaHAXPMHIMMC" apiKey="8b00405cba281a7d800ccec393e9af24" - dataSourceId="crawler-beta-react-rag" - promptId="crawler-betaHAXPMHIMMC-TDE" + askAi={true} insights={true} /> diff --git a/package.json b/package.json index 678fc578..4f6d406a 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,9 @@ "@babel/preset-typescript": "7.26.0", "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@rollup/plugin-babel": "6.0.4", + "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "6.0.2", "@rollup/plugin-terser": "0.4.4", "@stylistic/eslint-plugin": "2.13.0", diff --git a/packages/docsearch-css/src/modal.css b/packages/docsearch-css/src/modal.css index ec589b97..41aba06c 100644 --- a/packages/docsearch-css/src/modal.css +++ b/packages/docsearch-css/src/modal.css @@ -770,7 +770,7 @@ assistive tech users */ gap: 12px; font-size: 0.65em; font-weight: 300; - padding: 1em 0.4em; + padding: 1em 0; } .DocSearch-AskAi-Disclaimer { @@ -806,7 +806,7 @@ assistive tech users */ } .DocSearch-AskAiScreen-Query { - font-size: 1.2em; + font-size: 1.5em; font-weight: 600; margin: 0; } @@ -892,13 +892,13 @@ assistive tech users */ flex-direction: row; flex-wrap: wrap; width: 100%; - gap: 4px; + gap: 12px; } .DocSearch-AskAiScreen-RelatedSources-Title { font-size: 0.7em; font-weight: 400; - color: var(--docsearch-text-color); + color: var(--docsearch-secondary-color); padding: 6px 0; margin: 0; } @@ -921,7 +921,7 @@ assistive tech users */ display: flex; align-items: center; gap: 6px; - padding: 12px 6px; + padding: 12px 8px; background: var(--docsearch-hit-background); border-radius: 4px; color: var(--docsearch-text-color); @@ -947,6 +947,7 @@ assistive tech users */ .DocSearch-AskAiScreen-ExchangesList { gap: 24px; + margin: 8px 0; display: flex; flex-direction: column; } @@ -957,7 +958,13 @@ assistive tech users */ .DocSearch-Markdown-Content { color: var(--docsearch-text-color); - line-height: 1.5; + line-height: 1.6; + font-size: 1em; + letter-spacing: -0.011em; +} + +.DocSearch-Markdown-Content p { + margin: 1.2em 0; } .DocSearch-Markdown-Content p:last-child { @@ -974,15 +981,25 @@ assistive tech users */ padding: 0.2em 0.4em; margin: 0; border-radius: 3px; - font-family: monospace; + font-family: + ui-monospace, + SFMono-Regular, + SF Mono, + Menlo, + Consolas, + Liberation Mono, + monospace; + font-size: 0.9em; + letter-spacing: normal; } .DocSearch-Markdown-Content pre { background-color: var(--docsearch-key-background); color: var(--docsearch-text-color); - padding: 1em; - border-radius: 3px; + padding: 1.2em; + border-radius: 6px; overflow-x: auto; + margin: 1.5em 0; } .DocSearch-Markdown-Content pre code { @@ -990,10 +1007,11 @@ assistive tech users */ color: inherit; padding: 0; margin: 0; - font-size: inherit; + font-size: 0.8em; border-radius: 0; white-space: pre-wrap; word-wrap: break-word; + line-height: 1.5; } .DocSearch-Markdown-Content h1, @@ -1003,59 +1021,163 @@ assistive tech users */ .DocSearch-Markdown-Content h5, .DocSearch-Markdown-Content h6 { color: var(--docsearch-text-color); - margin-top: 1em; - margin-bottom: 0.5em; + margin: 2em 0 1em; font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.3; +} + +.DocSearch-Markdown-Content h1 { + font-size: 1.5em; +} + +.DocSearch-Markdown-Content h2 { + font-size: 1.3em; +} + +.DocSearch-Markdown-Content h3 { + font-size: 1.1em; +} + +.DocSearch-Markdown-Content h4 { + font-size: 1em; +} + +.DocSearch-Markdown-Content h5, +.DocSearch-Markdown-Content h6 { + font-size: 1em; } .DocSearch-Markdown-Content ul, .DocSearch-Markdown-Content ol { color: var(--docsearch-text-color); - margin-bottom: 1em; + margin: 1.2em 0; + padding-left: 1.5em; +} + +.DocSearch-Markdown-Content ul { + list-style-type: disc; +} + +.DocSearch-Markdown-Content ol { + list-style-type: decimal; } .DocSearch-Markdown-Content li { color: var(--docsearch-text-color); - margin-bottom: 0.25em; + margin: 0.5em 0; + padding-left: 0.3em; + line-height: 1.6; +} + +.DocSearch-Markdown-Content li > ul, +.DocSearch-Markdown-Content li > ol { + margin: 0.5em; } .DocSearch-Markdown-Content a { color: var(--docsearch-highlight-color); text-decoration: none; + transition: all 0.2s ease; } .DocSearch-Markdown-Content a:hover { text-decoration: underline; + opacity: 0.9; } -/* skeleton source styles */ -.DocSearch-AskAiScreen-SkeletonSource { +.DocSearch-Markdown-Content blockquote { + border-left: 4px solid var(--docsearch-hit-highlight-color); + margin: 1.5em 0; + padding: 0.5em 0 0.5em 1em; + color: var(--docsearch-secondary-text-color); + font-style: italic; +} + +.DocSearch-Markdown-Content hr { + border: none; + border-top: 1px solid var(--docsearch-subtle-color); + margin: 2em 0; +} + +.DocSearch-Markdown-Content table { + width: 100%; + border-collapse: collapse; + margin: 1.5em 0; +} + +.DocSearch-Markdown-Content th, +.DocSearch-Markdown-Content td { + padding: 0.75em; + border: 1px solid var(--docsearch-subtle-color); + text-align: left; +} + +.DocSearch-Markdown-Content th { + background-color: var(--docsearch-hit-background); + font-weight: 600; +} + +.DocSearch-AskAiScreen-MessageContent-Tool { display: flex; align-items: center; gap: 4px; - padding: 6px; - background: var(--docsearch-hit-background); - border-radius: 4px; - height: 32px; /* match item link height roughly */ + font-size: 0.9em; + padding: 1em 0; + color: var(--docsearch-secondary-text-color); } -.DocSearch-AskAiScreen-SkeletonSource-Icon { - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--docsearch-muted-color); - opacity: 0.4; - animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -.DocSearch-AskAiScreen-SkeletonSource-Text { - flex: 1 1 0; +.DocSearch-AskAiScreen-SmallerLoadingIcon { + width: 12px; height: 12px; - background: var(--docsearch-muted-color); - border-radius: 4px; - opacity: 0.4; - animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; - animation-delay: 0.2s; +} + +.shimmer { + position: relative; + overflow: hidden; +} + +.shimmer::before { + content: ''; + position: absolute; + top: 0; + left: -120%; + width: 60%; + height: 100%; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.1) 40%, + rgba(255, 255, 255, 0.8) 50%, + rgba(255, 255, 255, 0.1) 60%, + transparent 100% + ); + transform: skewX(-20deg); + animation: shimmer 6s ease-in-out infinite; + pointer-events: none; +} + +@keyframes shimmer { + 0% { + left: -120%; + } + 100% { + left: 120%; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.DocSearch-Markdown-Content > div { + opacity: 0; + animation: fadeIn 0.4s ease-out forwards; } @keyframes pulse { diff --git a/packages/docsearch-react/package.json b/packages/docsearch-react/package.json index 825bdec8..5a711a4a 100644 --- a/packages/docsearch-react/package.json +++ b/packages/docsearch-react/package.json @@ -36,11 +36,12 @@ "watch": "nodemon --watch src --ext ts,tsx,js,jsx,json --ignore dist/ --ignore node_modules/ --verbose --delay 250ms --exec \"yarn on:change\"" }, "dependencies": { - "@algolia/autocomplete-core": "1.18.1", - "@algolia/autocomplete-preset-algolia": "1.18.1", + "@ai-sdk/react": "^1.2.12", + "@algolia/autocomplete-core": "1.19.2", + "@algolia/autocomplete-preset-algolia": "1.19.2", "@docsearch/css": "3.9.0", - "algoliasearch": "^5.14.2", - "marked": "^15.0.11" + "algoliasearch": "^5.28.0", + "marked": "^15.0.12" }, "devDependencies": { "@rollup/plugin-replace": "6.0.2", diff --git a/packages/docsearch-react/rollup.config.js b/packages/docsearch-react/rollup.config.js index 4c93e266..6e57b7ea 100644 --- a/packages/docsearch-react/rollup.config.js +++ b/packages/docsearch-react/rollup.config.js @@ -1,3 +1,5 @@ +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import { plugins, typesConfig } from '../../rollup.base.config'; @@ -8,12 +10,13 @@ import pkg from './package.json'; export default [ { input: 'src/index.ts', - external: ['react', 'react-dom'], + external: ['react', 'react-dom', 'zod'], output: [ { globals: { react: 'React', 'react-dom': 'ReactDOM', + zod: 'Zod', }, file: 'dist/umd/index.js', format: 'umd', @@ -24,6 +27,8 @@ export default [ { dir: 'dist/esm', format: 'es' }, ], plugins: [ + resolve(), + commonjs(), ...plugins, replace({ preventAssignment: true, diff --git a/packages/docsearch-react/src/AskAiScreen.tsx b/packages/docsearch-react/src/AskAiScreen.tsx index f15b57f2..54aa6c58 100644 --- a/packages/docsearch-react/src/AskAiScreen.tsx +++ b/packages/docsearch-react/src/AskAiScreen.tsx @@ -1,26 +1,11 @@ +import type { UseChatHelpers } from '@ai-sdk/react'; import React, { type JSX, useState, useEffect, useMemo } from 'react'; -import { SparklesIcon } from './icons'; -import type { AskAiResponse } from './lib/genAiClient'; +import { SparklesIcon, LoadingIcon, SearchIcon } from './icons'; import { MemoizedMarkdown } from './MemoizedMarkdown'; import type { ScreenStateProps } from './ScreenState'; import type { InternalDocSearchHit } from './types'; - -interface Message { - id: string; - role: 'assistant' | 'user'; - content: string; - urls?: Array<{ url: string; title?: string }>; - context?: AskAiResponse['context']; -} - -type LoadingStatus = 'error' | 'idle' | 'loading' | 'streaming'; - -interface AskAiStateForScreen { - messages: Message[]; - loadingStatus: LoadingStatus; - error: Error | null; -} +import { extractLinksFromText } from './utils/ai'; export type AskAiScreenTranslations = Partial<{ titleText: string; @@ -29,8 +14,9 @@ export type AskAiScreenTranslations = Partial<{ thinkingText: string; }>; -type AskAiScreenProps = Omit, 'askAiState' | 'translations'> & { - askAiState: AskAiStateForScreen; +type AskAiScreenProps = Omit, 'translations'> & { + messages: UseChatHelpers['messages']; + status: UseChatHelpers['status']; translations?: AskAiScreenTranslations; }; @@ -40,8 +26,8 @@ interface AskAiScreenHeaderProps { interface Exchange { id: string; - userMessage: Message; - assistantMessage: Message | null; + userMessage: UseChatHelpers['messages'][number]; + assistantMessage: UseChatHelpers['messages'][number] | null; } function AskAiScreenHeader({ disclaimerText }: AskAiScreenHeaderProps): JSX.Element { @@ -58,27 +44,21 @@ function AskAiScreenHeader({ disclaimerText }: AskAiScreenHeaderProps): JSX.Elem interface AskAiExchangeCardProps { exchange: Exchange; isLastExchange: boolean; - loadingStatus: LoadingStatus; - error: Error | null; - translations: Required; - globalHasHadAssistantResponse: boolean; + loadingStatus: UseChatHelpers['status']; + translations: AskAiScreenTranslations; } function AskAiExchangeCard({ exchange, isLastExchange, loadingStatus, - error, translations, - globalHasHadAssistantResponse, }: AskAiExchangeCardProps): JSX.Element { const { userMessage, assistantMessage } = exchange; - const showLoadingIndicator = isLastExchange && loadingStatus === 'loading'; - const isStreaming = isLastExchange && loadingStatus === 'streaming'; - const showError = isLastExchange && loadingStatus === 'error' && error; - const showActions = !isLastExchange || (isLastExchange && loadingStatus === 'idle' && Boolean(assistantMessage)); - const urlsToDisplay = assistantMessage?.urls || []; + const showActions = !isLastExchange || (isLastExchange && loadingStatus === 'ready' && Boolean(assistantMessage)); + + const urlsToDisplay = extractLinksFromText(assistantMessage?.content || ''); return (
@@ -88,18 +68,61 @@ function AskAiExchangeCard({
- {showLoadingIndicator && ( -
- -
- )} - {(isStreaming || assistantMessage?.content) && ( - - )} - {showError &&

{error.message}

} + {Array.isArray(assistantMessage?.parts) + ? assistantMessage.parts.map((part, idx) => { + const index = idx; + if (part.type === 'text') { + return ( + + ); + } + if (part.type === 'tool-invocation') { + const { toolInvocation } = part; + if (toolInvocation.toolName === 'searchIndex') { + switch (toolInvocation.state) { + case 'partial-call': + return ( +
+ + Searching through the docs... +
+ ); + case 'call': + return ( +
+ + Searching through the docs for "{toolInvocation.args?.query || ''}" ... +
+ ); + case 'result': + return ( +
+ + Looked through the docs for "{toolInvocation.args?.query || ''}" +
+ ); + default: + return null; + } + } + // fallback for unknown tool + return ( + + Thinking... + + ); + } + // fallback for unknown part type + return null; + }) + : assistantMessage?.content}
@@ -111,19 +134,8 @@ function AskAiExchangeCard({
{/* Sources for this exchange */} - {urlsToDisplay.length > 0 || - (urlsToDisplay.length === 0 && - loadingStatus === 'loading' && - !globalHasHadAssistantResponse && - isLastExchange && - (loadingStatus === 'loading' || loadingStatus === 'streaming')) ? ( - + {urlsToDisplay.length > 0 ? ( + ) : null} ); @@ -152,22 +164,13 @@ function AskAiScreenFooterActions({ interface AskAiSourcesPanelProps { urlsToDisplay: Array<{ url: string; title?: string }>; - loadingStatus: LoadingStatus; - relatedSourcesText: string; - hasHadAssistantResponse: boolean; - isExchangeLoading: boolean; + relatedSourcesText?: string; } -function AskAiSourcesPanel({ - urlsToDisplay, - loadingStatus, - relatedSourcesText, - hasHadAssistantResponse, - isExchangeLoading, -}: AskAiSourcesPanelProps): JSX.Element { +function AskAiSourcesPanel({ urlsToDisplay, relatedSourcesText }: AskAiSourcesPanelProps): JSX.Element { return (
-

{relatedSourcesText}

+

{relatedSourcesText || 'Related sources'}

{urlsToDisplay.length > 0 && urlsToDisplay.map((link) => ( @@ -183,22 +186,6 @@ function AskAiSourcesPanel({ ))}
- {urlsToDisplay.length === 0 && - loadingStatus === 'loading' && - !hasHadAssistantResponse && - isExchangeLoading && - // eslint-disable-next-line react/no-array-index-key - Array.from({ length: 3 }).map((_, index) => )} - - {urlsToDisplay.length === 0 && - (loadingStatus === 'idle' || loadingStatus === 'streaming') && - hasHadAssistantResponse && - !isExchangeLoading && ( -

no related sources for the latest answer.

- )} - {urlsToDisplay.length === 0 && loadingStatus === 'error' && ( -

could not load related sources.

- )}
); } @@ -206,19 +193,9 @@ function AskAiSourcesPanel({ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): JSX.Element | null { const { disclaimerText = 'Answers are generated using artificial intelligence. This is an experimental technology, and information may occasionally be incorrect or misleading.', - relatedSourcesText = 'Related Sources', - thinkingText = 'Thinking', - titleText = 'Ask AI', } = translations; - const finalTranslations: Required = { - titleText, - disclaimerText, - relatedSourcesText, - thinkingText, - }; - - const { messages, loadingStatus, error } = props.askAiState; + const { messages } = props; // Group messages into exchanges (user + assistant pairs) const exchanges: Exchange[] = useMemo(() => { @@ -236,11 +213,8 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): return grouped; }, [messages]); - const globalHasHadAssistantResponse = messages.some((m) => m.role === 'assistant'); - return (
-
{exchanges @@ -251,45 +225,13 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): key={exchange.id} exchange={exchange} isLastExchange={index === 0} - loadingStatus={loadingStatus} - error={error} - translations={finalTranslations} - globalHasHadAssistantResponse={globalHasHadAssistantResponse} + loadingStatus={props.status} + translations={translations} /> ))}
-
- ); -} - -function ThinkingDots({ thinkingText }: { thinkingText: string }): JSX.Element { - const [dots, setDots] = useState(''); - - useEffect(() => { - const interval = setInterval(() => { - setDots((prevDots) => { - if (prevDots === '...') return ''; - return prevDots + '.'; - }); - }, 500); - - return (): void => clearInterval(interval); - }, []); - - return ( -

- {thinkingText} - {dots} -

- ); -} - -function SkeletonSource(): JSX.Element { - return ( -
- -
+
); } diff --git a/packages/docsearch-react/src/DocSearch.tsx b/packages/docsearch-react/src/DocSearch.tsx index 7abd05b1..b66b3060 100644 --- a/packages/docsearch-react/src/DocSearch.tsx +++ b/packages/docsearch-react/src/DocSearch.tsx @@ -22,12 +22,18 @@ export type DocSearchTransformClient = { transporter: Pick; }; +export type DocSearchAskAi = { + indexName: string; + apiKey: string; + appId: string; + promptId?: string | null; +}; + export interface DocSearchProps { appId: string; apiKey: string; indexName: string; - dataSourceId?: string; - promptId?: string; + askAi?: DocSearchAskAi | boolean; placeholder?: string; searchParameters?: SearchParamsObject; maxResultsPerGroup?: number; @@ -53,7 +59,7 @@ export function DocSearch(props: DocSearchProps): JSX.Element { props?.translations?.modal?.searchBox?.placeholderText || props?.placeholder || 'Search docs'; // check if the instance is configured to handle ask ai - const canHandleAskAi = Boolean(props?.dataSourceId && props?.promptId); + const canHandleAskAi = Boolean(props?.askAi); if (canHandleAskAi) { currentPlaceholder = props?.translations?.modal?.searchBox?.placeholderText || 'Search docs or ask AI a question'; diff --git a/packages/docsearch-react/src/DocSearchModal.tsx b/packages/docsearch-react/src/DocSearchModal.tsx index 2be38163..96339891 100644 --- a/packages/docsearch-react/src/DocSearchModal.tsx +++ b/packages/docsearch-react/src/DocSearchModal.tsx @@ -1,3 +1,5 @@ +import type { Message } from '@ai-sdk/react'; +import { useChat } from '@ai-sdk/react'; import { type AutocompleteSource, type AlgoliaInsightsHit, @@ -7,7 +9,7 @@ import { import type { SearchResponse } from 'algoliasearch/lite'; import React, { type JSX } from 'react'; -import { MAX_QUERY_SIZE } from './constants'; +import { ASK_AI_API_URL, MAX_QUERY_SIZE } from './constants'; import type { DocSearchProps } from './DocSearch'; import type { FooterTranslations } from './Footer'; import { Footer } from './Footer'; @@ -18,12 +20,11 @@ import type { SearchBoxTranslations } from './SearchBox'; import { SearchBox } from './SearchBox'; import { createStoredConversations, createStoredSearches } from './stored-searches'; import type { DocSearchHit, DocSearchState, InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types'; -import type { AskAiState } from './useAskAi'; -import { useAskAi, useGenAiClient } from './useAskAi'; import { useSearchClient } from './useSearchClient'; import { useTouchEvents } from './useTouchEvents'; import { useTrapFocus } from './useTrapFocus'; import { groupBy, identity, noop, removeHighlightTags, isModifierEvent } from './utils'; +import { buildDummyAskAiHit } from './utils/ai'; export type ModalTranslations = Partial<{ searchBox: SearchBoxTranslations; @@ -64,7 +65,7 @@ const buildNoQuerySources = ({ return []; } - const sources: Array> = [ + const sources: Array> = [ { sourceId: 'recentSearches', onSelect({ item, event }): void { @@ -258,10 +259,9 @@ const buildQuerySources = async ({ export function DocSearchModal({ appId, apiKey, - dataSourceId, - promptId, indexName, placeholder, + askAi, searchParameters, maxResultsPerGroup, onClose = noop, @@ -301,16 +301,6 @@ export function DocSearchModal({ typeof window !== 'undefined' ? window.getSelection()!.toString().slice(0, MAX_QUERY_SIZE) : '', ).current; const initialQuery = React.useRef(initialQueryFromProp || initialQueryFromSelection).current; - - const searchClient = useSearchClient(appId, apiKey, transformSearchClient); - const genAiClient = useGenAiClient(appId, apiKey, { - dataSourceId, - promptId, - }); - if (!genAiClient && canHandleAskAi) { - throw new Error('Something went wrong while initializing the Ask AI feature.'); - } - // storage const conversations = React.useRef( createStoredConversations({ @@ -333,12 +323,30 @@ export function DocSearchModal({ }), ).current; - // askAI - const askAiState = useAskAi({ - genAiClient: genAiClient!, - conversations, + const searchClient = useSearchClient(appId, apiKey, transformSearchClient); + + const askAiConfig = typeof askAi === 'object' ? askAi : null; + + const { messages, append, status, setMessages } = useChat({ + api: ASK_AI_API_URL, + headers: { + 'Content-Type': 'application/json', + 'X-Algolia-API-Key': askAiConfig?.apiKey || apiKey, + 'X-Algolia-Application-Id': askAiConfig?.appId || appId, + 'X-Algolia-Index-Name': askAiConfig?.indexName || indexName, + 'X-Documentation-Name': 'Documentation', + }, }); + const prevStatus = React.useRef(status); + React.useEffect(() => { + // if we just transitioned from "streaming" → "ready", persist + if (prevStatus.current === 'streaming' && status === 'ready') { + conversations.add(buildDummyAskAiHit(messages[0].content, messages)); + } + prevStatus.current = status; + }, [status, messages, conversations]); + const saveRecentSearch = React.useCallback( function saveRecentSearch(item: InternalDocSearchHit) { if (disableUserPersonalization) { @@ -388,22 +396,25 @@ export function DocSearchModal({ >(undefined); const handleAskAiToggle = React.useCallback( - (toggle: boolean, query: string) => { + async (toggle: boolean, query: string) => { onAskAiToggle(toggle); + await append({ + role: 'user', + content: query, + }); + // clear the query if (autocompleteRef.current) { autocompleteRef.current.setQuery(''); } - askAiState.ask?.({ query }); }, - [onAskAiToggle, askAiState], + [onAskAiToggle, append], ); if (!autocompleteRef.current) { autocompleteRef.current = createAutocomplete({ id: 'docsearch', - // we don't want to focus on the AskAI hit by default - defaultActiveItemId: canHandleAskAi ? 1 : 0, + defaultActiveItemId: 0, openOnFocus: true, initialState: { query: initialQuery, @@ -431,7 +442,7 @@ export function DocSearchModal({ canHandleAskAi, }); - const recentConversationSource: Array> = + const recentConversationSource: Array> = canHandleAskAi ? [ { @@ -440,8 +451,9 @@ export function DocSearchModal({ return conversations.getAll() as unknown as InternalDocSearchHit[]; }, onSelect({ item }): void { - if (item.askState) { - handleAskAiToggle(true, item.askState.query); + if (item.messages) { + setMessages(item.messages as any); + onAskAiToggle(true); } }, }, @@ -610,8 +622,9 @@ export function DocSearchModal({ React.useEffect(() => { if (!isAskAiActive) { autocomplete.refresh(); + setMessages([]); } - }, [isAskAiActive, autocomplete]); + }, [isAskAiActive, autocomplete, setMessages]); return (
{ // if the item is askAI toggle the screen if (item.type === 'askAI' && item.query) { - handleAskAiToggle(true, item.query); + // if the item is askAI and the anchor is stored + if (item.anchor === 'stored' && 'messages' in item) { + setMessages(item.messages as any); + onAskAiToggle(true); + } else { + handleAskAiToggle(true, item.query); + } event.preventDefault(); return; } diff --git a/packages/docsearch-react/src/ScreenState.tsx b/packages/docsearch-react/src/ScreenState.tsx index 9e5cbbef..ab62d10a 100644 --- a/packages/docsearch-react/src/ScreenState.tsx +++ b/packages/docsearch-react/src/ScreenState.tsx @@ -1,3 +1,4 @@ +import type { UseChatHelpers } from '@ai-sdk/react'; import type { AutocompleteApi, AutocompleteState, BaseItem } from '@algolia/autocomplete-core'; import React from 'react'; @@ -14,7 +15,6 @@ import type { StartScreenTranslations } from './StartScreen'; import { StartScreen } from './StartScreen'; import type { StoredSearchPlugin } from './stored-searches'; import type { InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types'; -import type { AskAiState } from './useAskAi'; export type ScreenStateTranslations = Partial<{ errorScreen: ErrorScreenTranslations; @@ -37,8 +37,9 @@ export interface ScreenStateProps inputRef: React.MutableRefObject; hitComponent: DocSearchProps['hitComponent']; indexName: DocSearchProps['indexName']; + messages: UseChatHelpers['messages']; + status: UseChatHelpers['status']; disableUserPersonalization: boolean; - askAiState?: AskAiState; resultsFooterComponent: DocSearchProps['resultsFooterComponent']; translations: ScreenStateTranslations; getMissingResultsUrl?: DocSearchProps['getMissingResultsUrl']; @@ -46,8 +47,15 @@ export interface ScreenStateProps export const ScreenState = React.memo( ({ translations = {}, ...props }: ScreenStateProps) => { - if (props.isAskAiActive && props.canHandleAskAi && props.askAiState) { - return ; + if (props.isAskAiActive && props.canHandleAskAi) { + return ( + + ); } if (props.state?.status === 'error') { diff --git a/packages/docsearch-react/src/constants.ts b/packages/docsearch-react/src/constants.ts index bbeca55a..7c951b96 100644 --- a/packages/docsearch-react/src/constants.ts +++ b/packages/docsearch-react/src/constants.ts @@ -1 +1,2 @@ -export const MAX_QUERY_SIZE = 64; +export const MAX_QUERY_SIZE = 256; +export const ASK_AI_API_URL = 'https://motivated-perfection-production.up.railway.app/chat'; diff --git a/packages/docsearch-react/src/icons/LoadingIcon.tsx b/packages/docsearch-react/src/icons/LoadingIcon.tsx index 88eed612..df1c02c2 100644 --- a/packages/docsearch-react/src/icons/LoadingIcon.tsx +++ b/packages/docsearch-react/src/icons/LoadingIcon.tsx @@ -1,8 +1,8 @@ import React, { type JSX } from 'react'; -export function LoadingIcon(): JSX.Element { +export function LoadingIcon({ className }: { className?: string }): JSX.Element { return ( - + diff --git a/packages/docsearch-react/src/lib/genAiClient.ts b/packages/docsearch-react/src/lib/genAiClient.ts deleted file mode 100644 index 385ec60d..00000000 --- a/packages/docsearch-react/src/lib/genAiClient.ts +++ /dev/null @@ -1,160 +0,0 @@ -export interface AskAiResponse { - response: string; - additionalFilters: string[]; - context: Array<{ title?: string; url?: string; objectID: string }>; - conversationID: string; - createdAt: string; - query: string; - metadata: Record; -} - -export interface GenAiClientOptions { - dataSourceId?: string; - promptId?: string; -} - -export interface GenAiClient { - appId: string; - apiKey: string; - dataSourceId?: string; - promptId?: string; - fetchAskAiResponse: (params: Omit) => Promise; -} - -const BASE_URL = 'https://generative-ai.algolia.com'; - -export function algoliaGenAiToolkit(appId: string, apiKey: string, options: GenAiClientOptions): GenAiClient { - const client: Omit = { - appId, - apiKey, - ...options, - }; - - return { - ...client, - fetchAskAiResponse: (params) => - fetchAskAiResponseFunction({ - ...params, - genAiClient: client as GenAiClient, - }), - }; -} - -export interface FetchAskAiResponseParams { - query: string; - genAiClient: GenAiClient; - conversationId?: string | null; - additionalFilters?: Record; - onUpdate: (chunk: AskAiResponse) => void; - onComplete?: () => void; - onError?: (error: Error) => void; -} - -async function fetchAskAiResponseFunction({ - query, - genAiClient, - additionalFilters, - conversationId, - onUpdate, - onComplete, - onError, -}: FetchAskAiResponseParams): Promise { - const { appId, apiKey, dataSourceId, promptId } = genAiClient; - let finalResponse: AskAiResponse | null = null; - - // Helper function to process a single SSE line - function processSseLine(line: string, context: string): AskAiResponse | null { - if (!line.startsWith('data:')) { - return null; - } - const jsonString = line.substring(5).trim(); - if (!jsonString) { - return null; - } - try { - const chunk = JSON.parse(jsonString) as AskAiResponse; - onUpdate(chunk); - return chunk; - } catch (e) { - if (onError) { - onError(e instanceof Error ? e : new Error(`failed to parse sse ${context}`)); - } - return null; - } - } - - try { - const response = await fetch(`${BASE_URL}/generate/response`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Algolia-Application-Id': appId, - 'X-Algolia-API-Key': apiKey, - }, - body: JSON.stringify({ - query, - dataSourceId, - promptId, - nbHits: 5, - useCache: false, - additionalFilters, - conversationId, - stream: true, - }), - }); - - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`Ask AI request failed with status ${response.status}: ${errorBody}`); - } - - if (!response.body) { - throw new Error('Response body is null'); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - // eslint-disable-next-line no-constant-condition - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - - buffer += decoder.decode(value, { stream: true }); - - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - const chunk = processSseLine(line, 'chunk'); - if (chunk) { - finalResponse = chunk; - } - } - } - - // Process any remaining data in the buffer - const finalChunk = processSseLine(buffer, 'final chunk'); - if (finalChunk) { - finalResponse = finalChunk; - } - - if (onComplete) { - onComplete(); - } - - if (!finalResponse) { - throw new Error('No valid response was received'); - } - - return finalResponse; - } catch (error) { - if (onError) { - onError(error instanceof Error ? error : new Error('unknown stream error')); - } - throw error; - } -} diff --git a/packages/docsearch-react/src/stored-searches.ts b/packages/docsearch-react/src/stored-searches.ts index 57cf2fdc..8d857ecf 100644 --- a/packages/docsearch-react/src/stored-searches.ts +++ b/packages/docsearch-react/src/stored-searches.ts @@ -54,14 +54,11 @@ export function createStoredConversations({ return { add(item: TItem): void { - const { askState } = item; + const { objectID, messages } = item; // check if this query is already saved - // @todo: this is a bit of a hack, we should be able to use - // conversationId to identify. const isQueryAlreadySaved = items.findIndex( - (x) => - x.objectID === askState?.conversationId || x.askState?.messages[0].content === askState?.messages[0].content, + (x) => x.objectID === objectID || x.messages?.[0]?.content === messages?.[0]?.content, ); if (isQueryAlreadySaved > -1) { diff --git a/packages/docsearch-react/src/types/StoredDocSearchHit.ts b/packages/docsearch-react/src/types/StoredDocSearchHit.ts index 3b34f8dc..036fbfd2 100644 --- a/packages/docsearch-react/src/types/StoredDocSearchHit.ts +++ b/packages/docsearch-react/src/types/StoredDocSearchHit.ts @@ -1,6 +1,6 @@ -import type { AskAiState } from '../useAskAi'; +import type { Message } from '@ai-sdk/react'; import type { DocSearchHit } from './DocSearchHit'; export type StoredDocSearchHit = Omit; -export type StoredAskAiState = Omit & { askState?: AskAiState }; +export type StoredAskAiState = Omit & { messages?: Message[] }; diff --git a/packages/docsearch-react/src/useAskAi.ts b/packages/docsearch-react/src/useAskAi.ts deleted file mode 100644 index 046c3bd8..00000000 --- a/packages/docsearch-react/src/useAskAi.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { useState, useCallback, useMemo, useRef } from 'react'; - -import { algoliaGenAiToolkit, type AskAiResponse, type GenAiClient, type GenAiClientOptions } from './lib/genAiClient'; -import type { StoredSearchPlugin } from './stored-searches'; -import type { StoredAskAiState } from './types'; - -type LoadingStatus = 'error' | 'idle' | 'loading' | 'streaming'; - -interface Message { - id: string; - role: 'assistant' | 'user'; - content: string; - context?: AskAiResponse['context']; - urls?: Array<{ url: string; title?: string }>; -} - -export interface AskAiState { - messages: Message[]; - query: string; - additionalFilters: string[]; - conversationId: string | null; - loadingStatus: LoadingStatus; - error: Error | null; - // optional just to make the type flexible - ask?: (params: AskParams) => Promise; - reset?: () => void; - restoreConversation?: (conversation: StoredAskAiState) => void; -} - -interface UseAskAiParams { - genAiClient: GenAiClient; - conversations: StoredSearchPlugin; -} - -interface AskParams { - query: string; - additionalFilters?: Record; -} - -/** - * Hook for interacting with Algolia's Generative AI API. - * - * @param params - Configuration options. - * @param params.genAiClient - The GenAI client instance. - * @param params.conversations - The conversations storage ref to store the AI responses. - * @returns State and functions for interacting with the AI. - */ -export function useAskAi({ genAiClient, conversations }: UseAskAiParams): AskAiState { - const initialState = useMemo( - () => ({ - messages: [], - query: '', - additionalFilters: [], - conversationId: null, - loadingStatus: 'idle' as const, - error: null, - }), - [], - ); - - const [state, setState] = useState>(initialState); - const didAddConversationRef = useRef(false); - - // reset state function - const reset = useCallback(() => { - setState(initialState); - didAddConversationRef.current = false; - }, [initialState]); - - const restoreConversation = useCallback( - (conversation: StoredAskAiState) => { - setState(conversation.askState ?? initialState); - didAddConversationRef.current = true; - }, - [initialState], - ); - - // ask ai request - const ask = useCallback( - async ({ query, additionalFilters }: AskParams) => { - // if there's no conversationid, empty the messages - if (!state.conversationId) { - setState((prevState) => ({ - ...prevState, - messages: [], - })); - } - - // generate a unique id for the user message - const userMessageId = crypto.randomUUID(); - const assistantMessageId = crypto.randomUUID(); - const newConversationId = state.conversationId ?? crypto.randomUUID(); - - // add user message to the conversation - setState((prevState) => ({ - ...prevState, - messages: [ - ...(prevState.conversationId ? prevState.messages : []), // keep history if we have a conversationId - { id: userMessageId, role: 'user', content: query }, - { id: assistantMessageId, role: 'assistant', content: '', context: [], urls: [] }, - ], - loadingStatus: 'loading', - query, - error: null, - })); - - try { - await genAiClient.fetchAskAiResponse({ - query, - additionalFilters, - // conversationId: newConversationId, - onUpdate: (chunk) => { - setState((prevState) => ({ - ...prevState, - messages: prevState.messages.map((m) => - m.id === assistantMessageId - ? { - ...m, - content: chunk.response, - context: chunk.context, - urls: extractLinksFromText(chunk.response), - } - : m, - ), - loadingStatus: 'streaming', - })); - }, - - onComplete: () => { - setState((prevState) => { - const newState = { - ...prevState, - loadingStatus: 'idle' as const, - conversationId: prevState.conversationId ?? newConversationId, - }; - - if (!didAddConversationRef.current) { - conversations.add({ - query: newState.messages[0].content, - objectID: newConversationId, - - // dummy content to make it a valid hit - // this is useful to show it among other hits - content: null, - hierarchy: { - lvl0: 'askAI', - lvl1: newState.messages[0].content, - lvl2: null, - lvl3: null, - lvl4: null, - lvl5: null, - lvl6: null, - }, - type: 'askAI', - url: '', - url_without_anchor: '', - anchor: '', - askState: newState, - }); - didAddConversationRef.current = true; - } - - return newState; - }); - }, - onError: (error) => { - // handle errors during the stream - setState((prevState) => ({ - ...prevState, - loadingStatus: 'error', - error, - })); - }, - }); - } catch (error) { - setState((prevState) => ({ - ...prevState, - loadingStatus: 'error', - error: error instanceof Error ? error : new Error('unknown fetch error'), - })); - } - }, - [genAiClient, conversations, state], - ); - - return { - ...state, - ask, - reset, - restoreConversation, - }; -} - -/** Function signature for transforming the GenAI client. */ -export type DocSearchTransformGenAiClient = (genAiClient: GenAiClient) => GenAiClient; - -/** - * Hook to create and memoize an Algolia Generative AI client instance. - * - * @param appId - Your Algolia Application ID. - * @param apiKey - Your Algolia API Key. - * @param options - GenAI client options (dataSourceId, promptID). - * @param transformGenAiClient - Optional function to modify the client instance. - * @returns A memoized GenAI client instance. - */ -export function useGenAiClient( - appId: string, - apiKey: string, - options: GenAiClientOptions, - transformGenAiClient: DocSearchTransformGenAiClient = (client) => client, -): GenAiClient | null { - const genAiClient = useMemo(() => { - if (!options.dataSourceId || !options.promptId) { - return null; - } - const client = algoliaGenAiToolkit(appId, apiKey, options); - - // note: Currently, the genAiClient doesn't have a built-in `addAlgoliaAgent` method like the search client. - // if needed in the future, agent logic would be added here. - - return transformGenAiClient(client); - }, [appId, apiKey, options, transformGenAiClient]); - - return genAiClient; -} - -// utility to extract links (markdown and bare urls) from a string -function extractLinksFromText(text: string): Array<{ url: string; title?: string }> { - // match [title](url) and bare urls - const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g; - const urlRegex = /https?:\/\/[^\s)]+/g; - const links: Array<{ url: string; title?: string }> = []; - const seen = new Set(); - - // extract markdown links first - let match; - while ((match = markdownLinkRegex.exec(text)) !== null) { - let url = match[2]; - const title = match[1]; - // trim trailing punctuation - url = url.replace(/[).,;!?]+$/, ''); - if (!seen.has(url)) { - links.push({ url, title }); - seen.add(url); - } - } - - // extract bare urls - while ((match = urlRegex.exec(text)) !== null) { - let url = match[0]; - url = url.replace(/[).,;!?]+$/, ''); - if (!seen.has(url)) { - links.push({ url }); - seen.add(url); - } - } - - return links; -} diff --git a/packages/docsearch-react/src/utils/ai.ts b/packages/docsearch-react/src/utils/ai.ts new file mode 100644 index 00000000..f7b05e63 --- /dev/null +++ b/packages/docsearch-react/src/utils/ai.ts @@ -0,0 +1,60 @@ +import type { Message } from '@ai-sdk/react'; + +import type { StoredAskAiState } from '../types'; + +// utility to extract links (markdown and bare urls) from a string +export function extractLinksFromText(text: string): Array<{ url: string; title?: string }> { + // match [title](url) and bare urls + const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g; + const urlRegex = /https?:\/\/[^\s)]+/g; + const links: Array<{ url: string; title?: string }> = []; + const seen = new Set(); + + // extract markdown links first + let match; + while ((match = markdownLinkRegex.exec(text)) !== null) { + let url = match[2]; + const title = match[1]; + // trim trailing punctuation + url = url.replace(/[).,;!?]+$/, ''); + if (!seen.has(url)) { + links.push({ url, title }); + seen.add(url); + } + } + + // extract bare urls + while ((match = urlRegex.exec(text)) !== null) { + let url = match[0]; + url = url.replace(/[).,;!?]+$/, ''); + if (!seen.has(url)) { + links.push({ url }); + seen.add(url); + } + } + + return links; +} + +export const buildDummyAskAiHit = (query: string, messages: Message[]): StoredAskAiState => ({ + query, + objectID: messages[0].content, + messages, + type: 'askAI', + anchor: 'stored', + + // dummy content to make it a valid hit + // this is useful to show it among other hits + content: null, + hierarchy: { + lvl0: 'askAI', + lvl1: messages[0].content, // use first message as hit name + lvl2: null, + lvl3: null, + lvl4: null, + lvl5: null, + lvl6: null, + }, + url: '', + url_without_anchor: '', +}); diff --git a/yarn.lock b/yarn.lock index 10460f5b..a6060445 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,59 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/provider-utils@npm:2.2.8": + version: 2.2.8 + resolution: "@ai-sdk/provider-utils@npm:2.2.8" + dependencies: + "@ai-sdk/provider": "npm:1.1.3" + nanoid: "npm:^3.3.8" + secure-json-parse: "npm:^2.7.0" + peerDependencies: + zod: ^3.23.8 + checksum: 10c0/34c72bf5f23f2d3e7aef496da7099422ba3b3ff243c35511853e16c3f1528717500262eea32b19e3e09bc4452152a5f31e650512f53f08a5f5645d907bff429e + languageName: node + linkType: hard + +"@ai-sdk/provider@npm:1.1.3": + version: 1.1.3 + resolution: "@ai-sdk/provider@npm:1.1.3" + dependencies: + json-schema: "npm:^0.4.0" + checksum: 10c0/40e080e223328e7c89829865e9c48f4ce8442a6a59f7ed5dfbdb4f63e8d859a76641e2d31e91970dd389bddb910f32ec7c3dbb0ce583c119e5a1e614ea7b8bc4 + languageName: node + linkType: hard + +"@ai-sdk/react@npm:^1.2.12": + version: 1.2.12 + resolution: "@ai-sdk/react@npm:1.2.12" + dependencies: + "@ai-sdk/provider-utils": "npm:2.2.8" + "@ai-sdk/ui-utils": "npm:1.2.11" + swr: "npm:^2.2.5" + throttleit: "npm:2.1.0" + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + checksum: 10c0/5422feb4ffeebd3287441cf658733e9ad7f9081fc279e85f57700d7fe9f4ed8a0504789c1be695790df44b28730e525cf12acf0f52bfa5adecc561ffd00cb2a5 + languageName: node + linkType: hard + +"@ai-sdk/ui-utils@npm:1.2.11": + version: 1.2.11 + resolution: "@ai-sdk/ui-utils@npm:1.2.11" + dependencies: + "@ai-sdk/provider": "npm:1.1.3" + "@ai-sdk/provider-utils": "npm:2.2.8" + zod-to-json-schema: "npm:^3.24.1" + peerDependencies: + zod: ^3.23.8 + checksum: 10c0/de0a10f9e16010126a21a1690aaf56d545b9c0f8d8b2cc33ffd22c2bb2e914949acb9b3f86e0e39a0e4b0d4f24db12e2b094045e34b311de0c8f84bfab48cc92 + languageName: node + linkType: hard + "@algolia/autocomplete-core@npm:1.18.1": version: 1.18.1 resolution: "@algolia/autocomplete-core@npm:1.18.1" @@ -2099,6 +2152,7 @@ __metadata: react: "npm:^19.0.0" react-dom: "npm:^19.0.0" vite: "npm:^6.0.7" + zod: "npm:^3.25.3" languageName: unknown linkType: soft @@ -2140,8 +2194,9 @@ __metadata: "@babel/preset-typescript": "npm:7.26.0" "@eslint-community/eslint-plugin-eslint-comments": "npm:4.4.1" "@rollup/plugin-babel": "npm:6.0.4" + "@rollup/plugin-commonjs": "npm:^28.0.3" "@rollup/plugin-json": "npm:6.1.0" - "@rollup/plugin-node-resolve": "npm:16.0.0" + "@rollup/plugin-node-resolve": "npm:^16.0.1" "@rollup/plugin-replace": "npm:6.0.2" "@rollup/plugin-terser": "npm:0.4.4" "@stylistic/eslint-plugin": "npm:2.13.0" @@ -2219,6 +2274,7 @@ __metadata: version: 0.0.0-use.local resolution: "@docsearch/react@workspace:packages/docsearch-react" dependencies: + "@ai-sdk/react": "npm:^1.2.12" "@algolia/autocomplete-core": "npm:1.18.1" "@algolia/autocomplete-preset-algolia": "npm:1.18.1" "@docsearch/css": "npm:3.9.0" @@ -4291,6 +4347,26 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-commonjs@npm:^28.0.3": + version: 28.0.3 + resolution: "@rollup/plugin-commonjs@npm:28.0.3" + dependencies: + "@rollup/pluginutils": "npm:^5.0.1" + commondir: "npm:^1.0.1" + estree-walker: "npm:^2.0.2" + fdir: "npm:^6.2.0" + is-reference: "npm:1.2.1" + magic-string: "npm:^0.30.3" + picomatch: "npm:^4.0.2" + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/0dbc61a5a894cdf526c3f0a738243cf1b3f634d592db0f2b9cad8c5d8e4c3e645d9b7ac19761c07a8a475118cafa6cdef4746111019388dd6e962f7373878ea4 + languageName: node + linkType: hard + "@rollup/plugin-json@npm:6.1.0": version: 6.1.0 resolution: "@rollup/plugin-json@npm:6.1.0" @@ -4305,9 +4381,9 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-node-resolve@npm:16.0.0": - version: 16.0.0 - resolution: "@rollup/plugin-node-resolve@npm:16.0.0" +"@rollup/plugin-node-resolve@npm:^16.0.1": + version: 16.0.1 + resolution: "@rollup/plugin-node-resolve@npm:16.0.1" dependencies: "@rollup/pluginutils": "npm:^5.0.1" "@types/resolve": "npm:1.20.2" @@ -4319,7 +4395,7 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 10c0/b63deb6fc14b37070ccaffacc8c10c9720f28ce7632f4fe2ee77064c0c79bcc3fe060fb77160e673c9fd847307252f25a2983030bd54f1888324063c69ae1399 + checksum: 10c0/54d33282321492fafec29b49c66dd1efd90c72a24f9d1569dcb57a72ab8de8a782810f39fdb917b96ec6a598c18f3416588b419bf7af331793a010de1fe28c60 languageName: node linkType: hard @@ -7907,6 +7983,13 @@ __metadata: languageName: node linkType: hard +"commondir@npm:^1.0.1": + version: 1.0.1 + resolution: "commondir@npm:1.0.1" + checksum: 10c0/33a124960e471c25ee19280c9ce31ccc19574b566dc514fe4f4ca4c34fa8b0b57cf437671f5de380e11353ea9426213fca17687dd2ef03134fea2dbc53809fd6 + languageName: node + linkType: hard + "compare-func@npm:^2.0.0": version: 2.0.0 resolution: "compare-func@npm:2.0.0" @@ -10796,6 +10879,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.2.0": + version: 6.4.4 + resolution: "fdir@npm:6.4.4" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/6ccc33be16945ee7bc841e1b4178c0b4cf18d3804894cb482aa514651c962a162f96da7ffc6ebfaf0df311689fb70091b04dd6caffe28d56b9ebdc0e7ccadfdd + languageName: node + linkType: hard + "feed@npm:^4.2.2": version: 4.2.2 resolution: "feed@npm:4.2.2" @@ -13022,6 +13117,15 @@ __metadata: languageName: node linkType: hard +"is-reference@npm:1.2.1": + version: 1.2.1 + resolution: "is-reference@npm:1.2.1" + dependencies: + "@types/estree": "npm:*" + checksum: 10c0/7dc819fc8de7790264a0a5d531164f9f5b9ef5aa1cd05f35322d14db39c8a2ec78fd5d4bf57f9789f3ddd2b3abeea7728432b759636157a42db12a9e8c3b549b + languageName: node + linkType: hard + "is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -13502,7 +13606,7 @@ __metadata: languageName: node linkType: hard -"json-schema@npm:0.4.0": +"json-schema@npm:0.4.0, json-schema@npm:^0.4.0": version: 0.4.0 resolution: "json-schema@npm:0.4.0" checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3 @@ -20358,6 +20462,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.7.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 + languageName: node + linkType: hard + "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -21798,6 +21909,18 @@ __metadata: languageName: node linkType: hard +"swr@npm:^2.2.5": + version: 2.3.3 + resolution: "swr@npm:2.3.3" + dependencies: + dequal: "npm:^2.0.3" + use-sync-external-store: "npm:^1.4.0" + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/882fc8291912860e0c50eae3470ebf0cd58b0144cb12adcc4b14c5cef913ea06479043830508d8b0b3d4061d99ad8dd52485c9c879fbd4e9b893484e6d8da9e3 + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -22021,6 +22144,13 @@ __metadata: languageName: node linkType: hard +"throttleit@npm:2.1.0": + version: 2.1.0 + resolution: "throttleit@npm:2.1.0" + checksum: 10c0/1696ae849522cea6ba4f4f3beac1f6655d335e51b42d99215e196a718adced0069e48deaaf77f7e89f526ab31de5b5c91016027da182438e6f9280be2f3d5265 + languageName: node + linkType: hard + "throttleit@npm:^1.0.0": version: 1.0.1 resolution: "throttleit@npm:1.0.1" @@ -22839,6 +22969,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.4.0": + version: 1.5.0 + resolution: "use-sync-external-store@npm:1.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/1b8663515c0be34fa653feb724fdcce3984037c78dd4a18f68b2c8be55cc1a1084c578d5b75f158d41b5ddffc2bf5600766d1af3c19c8e329bb20af2ec6f52f4 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -23859,6 +23998,22 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.24.1": + version: 3.24.5 + resolution: "zod-to-json-schema@npm:3.24.5" + peerDependencies: + zod: ^3.24.1 + checksum: 10c0/0745b94ba53e652d39f262641cdeb2f75d24339fb6076a38ce55bcf53d82dfaea63adf524ebc5f658681005401687f8e9551c4feca7c4c882e123e66091dfb90 + languageName: node + linkType: hard + +"zod@npm:^3.25.3": + version: 3.25.3 + resolution: "zod@npm:3.25.3" + checksum: 10c0/744bc6d8f1500fbda2ddc713540fea7db2354c585d256eed9064f11d89976c184e7472a999a6713d2ae8e148d9bc9f23aae0403c5eb57e45c8563cbd036aaa9d + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.4 resolution: "zwitch@npm:2.0.4"