1
0
Fork 0

fix: use algolia w tool calling

This commit is contained in:
Dylan Tientcheu 2025-06-18 13:08:09 +02:00
parent a2146c5910
commit f2cba37ea2
17 changed files with 550 additions and 652 deletions

View file

@ -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}
/>
</div>

View file

@ -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",

View file

@ -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 {

View file

@ -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",

View file

@ -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,

View file

@ -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<ScreenStateProps<InternalDocSearchHit>, 'askAiState' | 'translations'> & {
askAiState: AskAiStateForScreen;
type AskAiScreenProps = Omit<ScreenStateProps<InternalDocSearchHit>, '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<AskAiScreenTranslations>;
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 (
<div className="DocSearch-AskAiScreen-Response-Container">
@ -88,18 +68,61 @@ function AskAiExchangeCard({
</div>
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
<div className="DocSearch-AskAiScreen-MessageContent">
{showLoadingIndicator && (
<div className="DocSearch-AskAiScreen-Streaming-Loader">
<ThinkingDots thinkingText={translations.thinkingText} />
</div>
)}
{(isStreaming || assistantMessage?.content) && (
<MemoizedMarkdown
content={assistantMessage?.content || ''}
id={`ask-ai-message-${assistantMessage?.id || userMessage.id}`}
/>
)}
{showError && <p className="DocSearch-AskAiScreen-Error">{error.message}</p>}
{Array.isArray(assistantMessage?.parts)
? assistantMessage.parts.map((part, idx) => {
const index = idx;
if (part.type === 'text') {
return (
<MemoizedMarkdown
key={index}
content={part.text}
id={`ask-ai-message-${assistantMessage?.id}-${idx}`}
/>
);
}
if (part.type === 'tool-invocation') {
const { toolInvocation } = part;
if (toolInvocation.toolName === 'searchIndex') {
switch (toolInvocation.state) {
case 'partial-call':
return (
<div
key={index}
className="DocSearch-AskAiScreen-MessageContent-Tool Tool--PartialCall shimmer"
>
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
<span>Searching through the docs...</span>
</div>
);
case 'call':
return (
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Tool Tool--Call shimmer">
<LoadingIcon className="DocSearch-AskAiScreen-SmallerLoadingIcon" />
<span>Searching through the docs for "{toolInvocation.args?.query || ''}" ...</span>
</div>
);
case 'result':
return (
<div key={index} className="DocSearch-AskAiScreen-MessageContent-Tool Tool--Result">
<SearchIcon size={16} />
<span>Looked through the docs for "{toolInvocation.args?.query || ''}"</span>
</div>
);
default:
return null;
}
}
// fallback for unknown tool
return (
<span key={index} className="text-sm italic">
Thinking...
</span>
);
}
// fallback for unknown part type
return null;
})
: assistantMessage?.content}
</div>
</div>
<div className="DocSearch-AskAiScreen-Answer-Footer">
@ -111,19 +134,8 @@ function AskAiExchangeCard({
</div>
{/* Sources for this exchange */}
{urlsToDisplay.length > 0 ||
(urlsToDisplay.length === 0 &&
loadingStatus === 'loading' &&
!globalHasHadAssistantResponse &&
isLastExchange &&
(loadingStatus === 'loading' || loadingStatus === 'streaming')) ? (
<AskAiSourcesPanel
urlsToDisplay={urlsToDisplay}
loadingStatus={loadingStatus}
relatedSourcesText={translations.relatedSourcesText}
hasHadAssistantResponse={globalHasHadAssistantResponse}
isExchangeLoading={isLastExchange && (loadingStatus === 'loading' || loadingStatus === 'streaming')}
/>
{urlsToDisplay.length > 0 ? (
<AskAiSourcesPanel urlsToDisplay={urlsToDisplay} relatedSourcesText={translations.relatedSourcesText} />
) : null}
</div>
);
@ -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 (
<div className="DocSearch-AskAiScreen-RelatedSources">
<p className="DocSearch-AskAiScreen-RelatedSources-Title">{relatedSourcesText}</p>
<p className="DocSearch-AskAiScreen-RelatedSources-Title">{relatedSourcesText || 'Related sources'}</p>
<div className="DocSearch-AskAiScreen-RelatedSources-List">
{urlsToDisplay.length > 0 &&
urlsToDisplay.map((link) => (
@ -183,22 +186,6 @@ function AskAiSourcesPanel({
</a>
))}
</div>
{urlsToDisplay.length === 0 &&
loadingStatus === 'loading' &&
!hasHadAssistantResponse &&
isExchangeLoading &&
// eslint-disable-next-line react/no-array-index-key
Array.from({ length: 3 }).map((_, index) => <SkeletonSource key={index} />)}
{urlsToDisplay.length === 0 &&
(loadingStatus === 'idle' || loadingStatus === 'streaming') &&
hasHadAssistantResponse &&
!isExchangeLoading && (
<p className="DocSearch-AskAiScreen-RelatedSources-NoResults">no related sources for the latest answer.</p>
)}
{urlsToDisplay.length === 0 && loadingStatus === 'error' && (
<p className="DocSearch-AskAiScreen-RelatedSources-Error">could not load related sources.</p>
)}
</div>
);
}
@ -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<AskAiScreenTranslations> = {
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 (
<div className="DocSearch-AskAiScreen DocSearch-AskAiScreen-Container">
<AskAiScreenHeader disclaimerText={disclaimerText} />
<div className="DocSearch-AskAiScreen-Body">
<div className="DocSearch-AskAiScreen-ExchangesList">
{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}
/>
))}
</div>
</div>
</div>
);
}
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 (
<p className="DocSearch-AskAiScreen-ThinkingDots">
{thinkingText}
{dots}
</p>
);
}
function SkeletonSource(): JSX.Element {
return (
<div className="DocSearch-AskAiScreen-SkeletonSource">
<RelatedSourceIcon />
<div className="DocSearch-AskAiScreen-SkeletonSource-Text" />
<AskAiScreenHeader disclaimerText={disclaimerText} />
</div>
);
}

View file

@ -22,12 +22,18 @@ export type DocSearchTransformClient = {
transporter: Pick<LiteClient['transporter'], 'algoliaAgent'>;
};
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';

View file

@ -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<AutocompleteSource<InternalDocSearchHit & { askState?: StoredAskAiState }>> = [
const sources: Array<AutocompleteSource<InternalDocSearchHit>> = [
{
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<StoredAskAiState>({
@ -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<AutocompleteSource<InternalDocSearchHit & { askState?: AskAiState }>> =
const recentConversationSource: Array<AutocompleteSource<InternalDocSearchHit & { messages?: Message[] }>> =
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 (
<div
@ -668,12 +681,19 @@ export function DocSearchModal({
getMissingResultsUrl={getMissingResultsUrl}
isAskAiActive={isAskAiActive}
canHandleAskAi={canHandleAskAi}
askAiState={askAiState}
messages={messages}
status={status}
onAskAiToggle={onAskAiToggle}
onItemClick={(item, event) => {
// 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;
}

View file

@ -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<TItem extends BaseItem>
inputRef: React.MutableRefObject<HTMLInputElement | null>;
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<TItem extends BaseItem>
export const ScreenState = React.memo(
({ translations = {}, ...props }: ScreenStateProps<InternalDocSearchHit>) => {
if (props.isAskAiActive && props.canHandleAskAi && props.askAiState) {
return <AskAiScreen {...props} askAiState={props.askAiState} translations={translations?.askAiScreen} />;
if (props.isAskAiActive && props.canHandleAskAi) {
return (
<AskAiScreen
{...props}
messages={props.messages}
status={props.status}
translations={translations?.askAiScreen}
/>
);
}
if (props.state?.status === 'error') {

View file

@ -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';

View file

@ -1,8 +1,8 @@
import React, { type JSX } from 'react';
export function LoadingIcon(): JSX.Element {
export function LoadingIcon({ className }: { className?: string }): JSX.Element {
return (
<svg viewBox="0 0 38 38" stroke="currentColor" strokeOpacity=".5">
<svg viewBox="0 0 38 38" className={className} stroke="currentColor" strokeOpacity=".5">
<g fill="none" fillRule="evenodd">
<g transform="translate(1 1)" strokeWidth="2">
<circle strokeOpacity=".3" cx="18" cy="18" r="18" />

View file

@ -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<string, any>;
}
export interface GenAiClientOptions {
dataSourceId?: string;
promptId?: string;
}
export interface GenAiClient {
appId: string;
apiKey: string;
dataSourceId?: string;
promptId?: string;
fetchAskAiResponse: (params: Omit<FetchAskAiResponseParams, 'genAiClient'>) => Promise<AskAiResponse>;
}
const BASE_URL = 'https://generative-ai.algolia.com';
export function algoliaGenAiToolkit(appId: string, apiKey: string, options: GenAiClientOptions): GenAiClient {
const client: Omit<GenAiClient, 'fetchAskAiResponse'> = {
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<string, any>;
onUpdate: (chunk: AskAiResponse) => void;
onComplete?: () => void;
onError?: (error: Error) => void;
}
async function fetchAskAiResponseFunction({
query,
genAiClient,
additionalFilters,
conversationId,
onUpdate,
onComplete,
onError,
}: FetchAskAiResponseParams): Promise<AskAiResponse> {
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;
}
}

View file

@ -54,14 +54,11 @@ export function createStoredConversations<TItem extends StoredAskAiState>({
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) {

View file

@ -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<DocSearchHit, '_highlightResult' | '_snippetResult'>;
export type StoredAskAiState = Omit<DocSearchHit, '_highlightResult' | '_snippetResult'> & { askState?: AskAiState };
export type StoredAskAiState = Omit<DocSearchHit, '_highlightResult' | '_snippetResult'> & { messages?: Message[] };

View file

@ -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<void>;
reset?: () => void;
restoreConversation?: (conversation: StoredAskAiState) => void;
}
interface UseAskAiParams {
genAiClient: GenAiClient;
conversations: StoredSearchPlugin<StoredAskAiState>;
}
interface AskParams {
query: string;
additionalFilters?: Record<string, any>;
}
/**
* 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<Omit<AskAiState, 'ask' | 'reset'>>(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<string>();
// 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;
}

View file

@ -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<string>();
// 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: '',
});

167
yarn.lock
View file

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