fix: use algolia w tool calling
This commit is contained in:
parent
a2146c5910
commit
f2cba37ea2
17 changed files with 550 additions and 652 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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[] };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
60
packages/docsearch-react/src/utils/ai.ts
Normal file
60
packages/docsearch-react/src/utils/ai.ts
Normal 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
167
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue