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"
|
indexName="beta-react"
|
||||||
appId="betaHAXPMHIMMC"
|
appId="betaHAXPMHIMMC"
|
||||||
apiKey="8b00405cba281a7d800ccec393e9af24"
|
apiKey="8b00405cba281a7d800ccec393e9af24"
|
||||||
dataSourceId="crawler-beta-react-rag"
|
askAi={true}
|
||||||
promptId="crawler-betaHAXPMHIMMC-TDE"
|
|
||||||
insights={true}
|
insights={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,9 @@
|
||||||
"@babel/preset-typescript": "7.26.0",
|
"@babel/preset-typescript": "7.26.0",
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
|
||||||
"@rollup/plugin-babel": "6.0.4",
|
"@rollup/plugin-babel": "6.0.4",
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@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-replace": "6.0.2",
|
||||||
"@rollup/plugin-terser": "0.4.4",
|
"@rollup/plugin-terser": "0.4.4",
|
||||||
"@stylistic/eslint-plugin": "2.13.0",
|
"@stylistic/eslint-plugin": "2.13.0",
|
||||||
|
|
|
||||||
|
|
@ -770,7 +770,7 @@ assistive tech users */
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
font-size: 0.65em;
|
font-size: 0.65em;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
padding: 1em 0.4em;
|
padding: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-AskAi-Disclaimer {
|
.DocSearch-AskAi-Disclaimer {
|
||||||
|
|
@ -806,7 +806,7 @@ assistive tech users */
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-AskAiScreen-Query {
|
.DocSearch-AskAiScreen-Query {
|
||||||
font-size: 1.2em;
|
font-size: 1.5em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -892,13 +892,13 @@ assistive tech users */
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 4px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-AskAiScreen-RelatedSources-Title {
|
.DocSearch-AskAiScreen-RelatedSources-Title {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--docsearch-text-color);
|
color: var(--docsearch-secondary-color);
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -921,7 +921,7 @@ assistive tech users */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 12px 6px;
|
padding: 12px 8px;
|
||||||
background: var(--docsearch-hit-background);
|
background: var(--docsearch-hit-background);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--docsearch-text-color);
|
color: var(--docsearch-text-color);
|
||||||
|
|
@ -947,6 +947,7 @@ assistive tech users */
|
||||||
|
|
||||||
.DocSearch-AskAiScreen-ExchangesList {
|
.DocSearch-AskAiScreen-ExchangesList {
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
margin: 8px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
@ -957,7 +958,13 @@ assistive tech users */
|
||||||
|
|
||||||
.DocSearch-Markdown-Content {
|
.DocSearch-Markdown-Content {
|
||||||
color: var(--docsearch-text-color);
|
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 {
|
.DocSearch-Markdown-Content p:last-child {
|
||||||
|
|
@ -974,15 +981,25 @@ assistive tech users */
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 3px;
|
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 {
|
.DocSearch-Markdown-Content pre {
|
||||||
background-color: var(--docsearch-key-background);
|
background-color: var(--docsearch-key-background);
|
||||||
color: var(--docsearch-text-color);
|
color: var(--docsearch-text-color);
|
||||||
padding: 1em;
|
padding: 1.2em;
|
||||||
border-radius: 3px;
|
border-radius: 6px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
margin: 1.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-Markdown-Content pre code {
|
.DocSearch-Markdown-Content pre code {
|
||||||
|
|
@ -990,10 +1007,11 @@ assistive tech users */
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: inherit;
|
font-size: 0.8em;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-Markdown-Content h1,
|
.DocSearch-Markdown-Content h1,
|
||||||
|
|
@ -1003,59 +1021,163 @@ assistive tech users */
|
||||||
.DocSearch-Markdown-Content h5,
|
.DocSearch-Markdown-Content h5,
|
||||||
.DocSearch-Markdown-Content h6 {
|
.DocSearch-Markdown-Content h6 {
|
||||||
color: var(--docsearch-text-color);
|
color: var(--docsearch-text-color);
|
||||||
margin-top: 1em;
|
margin: 2em 0 1em;
|
||||||
margin-bottom: 0.5em;
|
|
||||||
font-weight: 600;
|
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 ul,
|
||||||
.DocSearch-Markdown-Content ol {
|
.DocSearch-Markdown-Content ol {
|
||||||
color: var(--docsearch-text-color);
|
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 {
|
.DocSearch-Markdown-Content li {
|
||||||
color: var(--docsearch-text-color);
|
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 {
|
.DocSearch-Markdown-Content a {
|
||||||
color: var(--docsearch-highlight-color);
|
color: var(--docsearch-highlight-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-Markdown-Content a:hover {
|
.DocSearch-Markdown-Content a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* skeleton source styles */
|
.DocSearch-Markdown-Content blockquote {
|
||||||
.DocSearch-AskAiScreen-SkeletonSource {
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 6px;
|
font-size: 0.9em;
|
||||||
background: var(--docsearch-hit-background);
|
padding: 1em 0;
|
||||||
border-radius: 4px;
|
color: var(--docsearch-secondary-text-color);
|
||||||
height: 32px; /* match item link height roughly */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.DocSearch-AskAiScreen-SkeletonSource-Icon {
|
.DocSearch-AskAiScreen-SmallerLoadingIcon {
|
||||||
width: 20px;
|
width: 12px;
|
||||||
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;
|
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background: var(--docsearch-muted-color);
|
}
|
||||||
border-radius: 4px;
|
|
||||||
opacity: 0.4;
|
.shimmer {
|
||||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
position: relative;
|
||||||
animation-delay: 0.2s;
|
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 {
|
@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\""
|
"watch": "nodemon --watch src --ext ts,tsx,js,jsx,json --ignore dist/ --ignore node_modules/ --verbose --delay 250ms --exec \"yarn on:change\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@algolia/autocomplete-core": "1.18.1",
|
"@ai-sdk/react": "^1.2.12",
|
||||||
"@algolia/autocomplete-preset-algolia": "1.18.1",
|
"@algolia/autocomplete-core": "1.19.2",
|
||||||
|
"@algolia/autocomplete-preset-algolia": "1.19.2",
|
||||||
"@docsearch/css": "3.9.0",
|
"@docsearch/css": "3.9.0",
|
||||||
"algoliasearch": "^5.14.2",
|
"algoliasearch": "^5.28.0",
|
||||||
"marked": "^15.0.11"
|
"marked": "^15.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@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 replace from '@rollup/plugin-replace';
|
||||||
|
|
||||||
import { plugins, typesConfig } from '../../rollup.base.config';
|
import { plugins, typesConfig } from '../../rollup.base.config';
|
||||||
|
|
@ -8,12 +10,13 @@ import pkg from './package.json';
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
input: 'src/index.ts',
|
input: 'src/index.ts',
|
||||||
external: ['react', 'react-dom'],
|
external: ['react', 'react-dom', 'zod'],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
globals: {
|
globals: {
|
||||||
react: 'React',
|
react: 'React',
|
||||||
'react-dom': 'ReactDOM',
|
'react-dom': 'ReactDOM',
|
||||||
|
zod: 'Zod',
|
||||||
},
|
},
|
||||||
file: 'dist/umd/index.js',
|
file: 'dist/umd/index.js',
|
||||||
format: 'umd',
|
format: 'umd',
|
||||||
|
|
@ -24,6 +27,8 @@ export default [
|
||||||
{ dir: 'dist/esm', format: 'es' },
|
{ dir: 'dist/esm', format: 'es' },
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
commonjs(),
|
||||||
...plugins,
|
...plugins,
|
||||||
replace({
|
replace({
|
||||||
preventAssignment: true,
|
preventAssignment: true,
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,11 @@
|
||||||
|
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||||
import React, { type JSX, useState, useEffect, useMemo } from 'react';
|
import React, { type JSX, useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { SparklesIcon } from './icons';
|
import { SparklesIcon, LoadingIcon, SearchIcon } from './icons';
|
||||||
import type { AskAiResponse } from './lib/genAiClient';
|
|
||||||
import { MemoizedMarkdown } from './MemoizedMarkdown';
|
import { MemoizedMarkdown } from './MemoizedMarkdown';
|
||||||
import type { ScreenStateProps } from './ScreenState';
|
import type { ScreenStateProps } from './ScreenState';
|
||||||
import type { InternalDocSearchHit } from './types';
|
import type { InternalDocSearchHit } from './types';
|
||||||
|
import { extractLinksFromText } from './utils/ai';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AskAiScreenTranslations = Partial<{
|
export type AskAiScreenTranslations = Partial<{
|
||||||
titleText: string;
|
titleText: string;
|
||||||
|
|
@ -29,8 +14,9 @@ export type AskAiScreenTranslations = Partial<{
|
||||||
thinkingText: string;
|
thinkingText: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type AskAiScreenProps = Omit<ScreenStateProps<InternalDocSearchHit>, 'askAiState' | 'translations'> & {
|
type AskAiScreenProps = Omit<ScreenStateProps<InternalDocSearchHit>, 'translations'> & {
|
||||||
askAiState: AskAiStateForScreen;
|
messages: UseChatHelpers['messages'];
|
||||||
|
status: UseChatHelpers['status'];
|
||||||
translations?: AskAiScreenTranslations;
|
translations?: AskAiScreenTranslations;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -40,8 +26,8 @@ interface AskAiScreenHeaderProps {
|
||||||
|
|
||||||
interface Exchange {
|
interface Exchange {
|
||||||
id: string;
|
id: string;
|
||||||
userMessage: Message;
|
userMessage: UseChatHelpers['messages'][number];
|
||||||
assistantMessage: Message | null;
|
assistantMessage: UseChatHelpers['messages'][number] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AskAiScreenHeader({ disclaimerText }: AskAiScreenHeaderProps): JSX.Element {
|
function AskAiScreenHeader({ disclaimerText }: AskAiScreenHeaderProps): JSX.Element {
|
||||||
|
|
@ -58,27 +44,21 @@ function AskAiScreenHeader({ disclaimerText }: AskAiScreenHeaderProps): JSX.Elem
|
||||||
interface AskAiExchangeCardProps {
|
interface AskAiExchangeCardProps {
|
||||||
exchange: Exchange;
|
exchange: Exchange;
|
||||||
isLastExchange: boolean;
|
isLastExchange: boolean;
|
||||||
loadingStatus: LoadingStatus;
|
loadingStatus: UseChatHelpers['status'];
|
||||||
error: Error | null;
|
translations: AskAiScreenTranslations;
|
||||||
translations: Required<AskAiScreenTranslations>;
|
|
||||||
globalHasHadAssistantResponse: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AskAiExchangeCard({
|
function AskAiExchangeCard({
|
||||||
exchange,
|
exchange,
|
||||||
isLastExchange,
|
isLastExchange,
|
||||||
loadingStatus,
|
loadingStatus,
|
||||||
error,
|
|
||||||
translations,
|
translations,
|
||||||
globalHasHadAssistantResponse,
|
|
||||||
}: AskAiExchangeCardProps): JSX.Element {
|
}: AskAiExchangeCardProps): JSX.Element {
|
||||||
const { userMessage, assistantMessage } = exchange;
|
const { userMessage, assistantMessage } = exchange;
|
||||||
|
|
||||||
const showLoadingIndicator = isLastExchange && loadingStatus === 'loading';
|
const showActions = !isLastExchange || (isLastExchange && loadingStatus === 'ready' && Boolean(assistantMessage));
|
||||||
const isStreaming = isLastExchange && loadingStatus === 'streaming';
|
|
||||||
const showError = isLastExchange && loadingStatus === 'error' && error;
|
const urlsToDisplay = extractLinksFromText(assistantMessage?.content || '');
|
||||||
const showActions = !isLastExchange || (isLastExchange && loadingStatus === 'idle' && Boolean(assistantMessage));
|
|
||||||
const urlsToDisplay = assistantMessage?.urls || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="DocSearch-AskAiScreen-Response-Container">
|
<div className="DocSearch-AskAiScreen-Response-Container">
|
||||||
|
|
@ -88,18 +68,61 @@ function AskAiExchangeCard({
|
||||||
</div>
|
</div>
|
||||||
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
|
<div className="DocSearch-AskAiScreen-Message DocSearch-AskAiScreen-Message--assistant">
|
||||||
<div className="DocSearch-AskAiScreen-MessageContent">
|
<div className="DocSearch-AskAiScreen-MessageContent">
|
||||||
{showLoadingIndicator && (
|
{Array.isArray(assistantMessage?.parts)
|
||||||
<div className="DocSearch-AskAiScreen-Streaming-Loader">
|
? assistantMessage.parts.map((part, idx) => {
|
||||||
<ThinkingDots thinkingText={translations.thinkingText} />
|
const index = idx;
|
||||||
</div>
|
if (part.type === 'text') {
|
||||||
)}
|
return (
|
||||||
{(isStreaming || assistantMessage?.content) && (
|
<MemoizedMarkdown
|
||||||
<MemoizedMarkdown
|
key={index}
|
||||||
content={assistantMessage?.content || ''}
|
content={part.text}
|
||||||
id={`ask-ai-message-${assistantMessage?.id || userMessage.id}`}
|
id={`ask-ai-message-${assistantMessage?.id}-${idx}`}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
{showError && <p className="DocSearch-AskAiScreen-Error">{error.message}</p>}
|
}
|
||||||
|
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>
|
</div>
|
||||||
<div className="DocSearch-AskAiScreen-Answer-Footer">
|
<div className="DocSearch-AskAiScreen-Answer-Footer">
|
||||||
|
|
@ -111,19 +134,8 @@ function AskAiExchangeCard({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sources for this exchange */}
|
{/* Sources for this exchange */}
|
||||||
{urlsToDisplay.length > 0 ||
|
{urlsToDisplay.length > 0 ? (
|
||||||
(urlsToDisplay.length === 0 &&
|
<AskAiSourcesPanel urlsToDisplay={urlsToDisplay} relatedSourcesText={translations.relatedSourcesText} />
|
||||||
loadingStatus === 'loading' &&
|
|
||||||
!globalHasHadAssistantResponse &&
|
|
||||||
isLastExchange &&
|
|
||||||
(loadingStatus === 'loading' || loadingStatus === 'streaming')) ? (
|
|
||||||
<AskAiSourcesPanel
|
|
||||||
urlsToDisplay={urlsToDisplay}
|
|
||||||
loadingStatus={loadingStatus}
|
|
||||||
relatedSourcesText={translations.relatedSourcesText}
|
|
||||||
hasHadAssistantResponse={globalHasHadAssistantResponse}
|
|
||||||
isExchangeLoading={isLastExchange && (loadingStatus === 'loading' || loadingStatus === 'streaming')}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -152,22 +164,13 @@ function AskAiScreenFooterActions({
|
||||||
|
|
||||||
interface AskAiSourcesPanelProps {
|
interface AskAiSourcesPanelProps {
|
||||||
urlsToDisplay: Array<{ url: string; title?: string }>;
|
urlsToDisplay: Array<{ url: string; title?: string }>;
|
||||||
loadingStatus: LoadingStatus;
|
relatedSourcesText?: string;
|
||||||
relatedSourcesText: string;
|
|
||||||
hasHadAssistantResponse: boolean;
|
|
||||||
isExchangeLoading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AskAiSourcesPanel({
|
function AskAiSourcesPanel({ urlsToDisplay, relatedSourcesText }: AskAiSourcesPanelProps): JSX.Element {
|
||||||
urlsToDisplay,
|
|
||||||
loadingStatus,
|
|
||||||
relatedSourcesText,
|
|
||||||
hasHadAssistantResponse,
|
|
||||||
isExchangeLoading,
|
|
||||||
}: AskAiSourcesPanelProps): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div className="DocSearch-AskAiScreen-RelatedSources">
|
<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">
|
<div className="DocSearch-AskAiScreen-RelatedSources-List">
|
||||||
{urlsToDisplay.length > 0 &&
|
{urlsToDisplay.length > 0 &&
|
||||||
urlsToDisplay.map((link) => (
|
urlsToDisplay.map((link) => (
|
||||||
|
|
@ -183,22 +186,6 @@ function AskAiSourcesPanel({
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -206,19 +193,9 @@ function AskAiSourcesPanel({
|
||||||
export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): JSX.Element | null {
|
export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
disclaimerText = 'Answers are generated using artificial intelligence. This is an experimental technology, and information may occasionally be incorrect or misleading.',
|
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;
|
} = translations;
|
||||||
|
|
||||||
const finalTranslations: Required<AskAiScreenTranslations> = {
|
const { messages } = props;
|
||||||
titleText,
|
|
||||||
disclaimerText,
|
|
||||||
relatedSourcesText,
|
|
||||||
thinkingText,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { messages, loadingStatus, error } = props.askAiState;
|
|
||||||
|
|
||||||
// Group messages into exchanges (user + assistant pairs)
|
// Group messages into exchanges (user + assistant pairs)
|
||||||
const exchanges: Exchange[] = useMemo(() => {
|
const exchanges: Exchange[] = useMemo(() => {
|
||||||
|
|
@ -236,11 +213,8 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
|
||||||
return grouped;
|
return grouped;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const globalHasHadAssistantResponse = messages.some((m) => m.role === 'assistant');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="DocSearch-AskAiScreen DocSearch-AskAiScreen-Container">
|
<div className="DocSearch-AskAiScreen DocSearch-AskAiScreen-Container">
|
||||||
<AskAiScreenHeader disclaimerText={disclaimerText} />
|
|
||||||
<div className="DocSearch-AskAiScreen-Body">
|
<div className="DocSearch-AskAiScreen-Body">
|
||||||
<div className="DocSearch-AskAiScreen-ExchangesList">
|
<div className="DocSearch-AskAiScreen-ExchangesList">
|
||||||
{exchanges
|
{exchanges
|
||||||
|
|
@ -251,45 +225,13 @@ export function AskAiScreen({ translations = {}, ...props }: AskAiScreenProps):
|
||||||
key={exchange.id}
|
key={exchange.id}
|
||||||
exchange={exchange}
|
exchange={exchange}
|
||||||
isLastExchange={index === 0}
|
isLastExchange={index === 0}
|
||||||
loadingStatus={loadingStatus}
|
loadingStatus={props.status}
|
||||||
error={error}
|
translations={translations}
|
||||||
translations={finalTranslations}
|
|
||||||
globalHasHadAssistantResponse={globalHasHadAssistantResponse}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<AskAiScreenHeader disclaimerText={disclaimerText} />
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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" />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,18 @@ export type DocSearchTransformClient = {
|
||||||
transporter: Pick<LiteClient['transporter'], 'algoliaAgent'>;
|
transporter: Pick<LiteClient['transporter'], 'algoliaAgent'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DocSearchAskAi = {
|
||||||
|
indexName: string;
|
||||||
|
apiKey: string;
|
||||||
|
appId: string;
|
||||||
|
promptId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export interface DocSearchProps {
|
export interface DocSearchProps {
|
||||||
appId: string;
|
appId: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
indexName: string;
|
indexName: string;
|
||||||
dataSourceId?: string;
|
askAi?: DocSearchAskAi | boolean;
|
||||||
promptId?: string;
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
searchParameters?: SearchParamsObject;
|
searchParameters?: SearchParamsObject;
|
||||||
maxResultsPerGroup?: number;
|
maxResultsPerGroup?: number;
|
||||||
|
|
@ -53,7 +59,7 @@ export function DocSearch(props: DocSearchProps): JSX.Element {
|
||||||
props?.translations?.modal?.searchBox?.placeholderText || props?.placeholder || 'Search docs';
|
props?.translations?.modal?.searchBox?.placeholderText || props?.placeholder || 'Search docs';
|
||||||
|
|
||||||
// check if the instance is configured to handle ask ai
|
// check if the instance is configured to handle ask ai
|
||||||
const canHandleAskAi = Boolean(props?.dataSourceId && props?.promptId);
|
const canHandleAskAi = Boolean(props?.askAi);
|
||||||
|
|
||||||
if (canHandleAskAi) {
|
if (canHandleAskAi) {
|
||||||
currentPlaceholder = props?.translations?.modal?.searchBox?.placeholderText || 'Search docs or ask AI a question';
|
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 {
|
import {
|
||||||
type AutocompleteSource,
|
type AutocompleteSource,
|
||||||
type AlgoliaInsightsHit,
|
type AlgoliaInsightsHit,
|
||||||
|
|
@ -7,7 +9,7 @@ import {
|
||||||
import type { SearchResponse } from 'algoliasearch/lite';
|
import type { SearchResponse } from 'algoliasearch/lite';
|
||||||
import React, { type JSX } from 'react';
|
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 { DocSearchProps } from './DocSearch';
|
||||||
import type { FooterTranslations } from './Footer';
|
import type { FooterTranslations } from './Footer';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
|
|
@ -18,12 +20,11 @@ import type { SearchBoxTranslations } from './SearchBox';
|
||||||
import { SearchBox } from './SearchBox';
|
import { SearchBox } from './SearchBox';
|
||||||
import { createStoredConversations, createStoredSearches } from './stored-searches';
|
import { createStoredConversations, createStoredSearches } from './stored-searches';
|
||||||
import type { DocSearchHit, DocSearchState, InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types';
|
import type { DocSearchHit, DocSearchState, InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types';
|
||||||
import type { AskAiState } from './useAskAi';
|
|
||||||
import { useAskAi, useGenAiClient } from './useAskAi';
|
|
||||||
import { useSearchClient } from './useSearchClient';
|
import { useSearchClient } from './useSearchClient';
|
||||||
import { useTouchEvents } from './useTouchEvents';
|
import { useTouchEvents } from './useTouchEvents';
|
||||||
import { useTrapFocus } from './useTrapFocus';
|
import { useTrapFocus } from './useTrapFocus';
|
||||||
import { groupBy, identity, noop, removeHighlightTags, isModifierEvent } from './utils';
|
import { groupBy, identity, noop, removeHighlightTags, isModifierEvent } from './utils';
|
||||||
|
import { buildDummyAskAiHit } from './utils/ai';
|
||||||
|
|
||||||
export type ModalTranslations = Partial<{
|
export type ModalTranslations = Partial<{
|
||||||
searchBox: SearchBoxTranslations;
|
searchBox: SearchBoxTranslations;
|
||||||
|
|
@ -64,7 +65,7 @@ const buildNoQuerySources = ({
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sources: Array<AutocompleteSource<InternalDocSearchHit & { askState?: StoredAskAiState }>> = [
|
const sources: Array<AutocompleteSource<InternalDocSearchHit>> = [
|
||||||
{
|
{
|
||||||
sourceId: 'recentSearches',
|
sourceId: 'recentSearches',
|
||||||
onSelect({ item, event }): void {
|
onSelect({ item, event }): void {
|
||||||
|
|
@ -258,10 +259,9 @@ const buildQuerySources = async ({
|
||||||
export function DocSearchModal({
|
export function DocSearchModal({
|
||||||
appId,
|
appId,
|
||||||
apiKey,
|
apiKey,
|
||||||
dataSourceId,
|
|
||||||
promptId,
|
|
||||||
indexName,
|
indexName,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
askAi,
|
||||||
searchParameters,
|
searchParameters,
|
||||||
maxResultsPerGroup,
|
maxResultsPerGroup,
|
||||||
onClose = noop,
|
onClose = noop,
|
||||||
|
|
@ -301,16 +301,6 @@ export function DocSearchModal({
|
||||||
typeof window !== 'undefined' ? window.getSelection()!.toString().slice(0, MAX_QUERY_SIZE) : '',
|
typeof window !== 'undefined' ? window.getSelection()!.toString().slice(0, MAX_QUERY_SIZE) : '',
|
||||||
).current;
|
).current;
|
||||||
const initialQuery = React.useRef(initialQueryFromProp || initialQueryFromSelection).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
|
// storage
|
||||||
const conversations = React.useRef(
|
const conversations = React.useRef(
|
||||||
createStoredConversations<StoredAskAiState>({
|
createStoredConversations<StoredAskAiState>({
|
||||||
|
|
@ -333,12 +323,30 @@ export function DocSearchModal({
|
||||||
}),
|
}),
|
||||||
).current;
|
).current;
|
||||||
|
|
||||||
// askAI
|
const searchClient = useSearchClient(appId, apiKey, transformSearchClient);
|
||||||
const askAiState = useAskAi({
|
|
||||||
genAiClient: genAiClient!,
|
const askAiConfig = typeof askAi === 'object' ? askAi : null;
|
||||||
conversations,
|
|
||||||
|
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(
|
const saveRecentSearch = React.useCallback(
|
||||||
function saveRecentSearch(item: InternalDocSearchHit) {
|
function saveRecentSearch(item: InternalDocSearchHit) {
|
||||||
if (disableUserPersonalization) {
|
if (disableUserPersonalization) {
|
||||||
|
|
@ -388,22 +396,25 @@ export function DocSearchModal({
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
const handleAskAiToggle = React.useCallback(
|
const handleAskAiToggle = React.useCallback(
|
||||||
(toggle: boolean, query: string) => {
|
async (toggle: boolean, query: string) => {
|
||||||
onAskAiToggle(toggle);
|
onAskAiToggle(toggle);
|
||||||
|
await append({
|
||||||
|
role: 'user',
|
||||||
|
content: query,
|
||||||
|
});
|
||||||
|
|
||||||
// clear the query
|
// clear the query
|
||||||
if (autocompleteRef.current) {
|
if (autocompleteRef.current) {
|
||||||
autocompleteRef.current.setQuery('');
|
autocompleteRef.current.setQuery('');
|
||||||
}
|
}
|
||||||
askAiState.ask?.({ query });
|
|
||||||
},
|
},
|
||||||
[onAskAiToggle, askAiState],
|
[onAskAiToggle, append],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!autocompleteRef.current) {
|
if (!autocompleteRef.current) {
|
||||||
autocompleteRef.current = createAutocomplete({
|
autocompleteRef.current = createAutocomplete({
|
||||||
id: 'docsearch',
|
id: 'docsearch',
|
||||||
// we don't want to focus on the AskAI hit by default
|
defaultActiveItemId: 0,
|
||||||
defaultActiveItemId: canHandleAskAi ? 1 : 0,
|
|
||||||
openOnFocus: true,
|
openOnFocus: true,
|
||||||
initialState: {
|
initialState: {
|
||||||
query: initialQuery,
|
query: initialQuery,
|
||||||
|
|
@ -431,7 +442,7 @@ export function DocSearchModal({
|
||||||
canHandleAskAi,
|
canHandleAskAi,
|
||||||
});
|
});
|
||||||
|
|
||||||
const recentConversationSource: Array<AutocompleteSource<InternalDocSearchHit & { askState?: AskAiState }>> =
|
const recentConversationSource: Array<AutocompleteSource<InternalDocSearchHit & { messages?: Message[] }>> =
|
||||||
canHandleAskAi
|
canHandleAskAi
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|
@ -440,8 +451,9 @@ export function DocSearchModal({
|
||||||
return conversations.getAll() as unknown as InternalDocSearchHit[];
|
return conversations.getAll() as unknown as InternalDocSearchHit[];
|
||||||
},
|
},
|
||||||
onSelect({ item }): void {
|
onSelect({ item }): void {
|
||||||
if (item.askState) {
|
if (item.messages) {
|
||||||
handleAskAiToggle(true, item.askState.query);
|
setMessages(item.messages as any);
|
||||||
|
onAskAiToggle(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -610,8 +622,9 @@ export function DocSearchModal({
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isAskAiActive) {
|
if (!isAskAiActive) {
|
||||||
autocomplete.refresh();
|
autocomplete.refresh();
|
||||||
|
setMessages([]);
|
||||||
}
|
}
|
||||||
}, [isAskAiActive, autocomplete]);
|
}, [isAskAiActive, autocomplete, setMessages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -668,12 +681,19 @@ export function DocSearchModal({
|
||||||
getMissingResultsUrl={getMissingResultsUrl}
|
getMissingResultsUrl={getMissingResultsUrl}
|
||||||
isAskAiActive={isAskAiActive}
|
isAskAiActive={isAskAiActive}
|
||||||
canHandleAskAi={canHandleAskAi}
|
canHandleAskAi={canHandleAskAi}
|
||||||
askAiState={askAiState}
|
messages={messages}
|
||||||
|
status={status}
|
||||||
onAskAiToggle={onAskAiToggle}
|
onAskAiToggle={onAskAiToggle}
|
||||||
onItemClick={(item, event) => {
|
onItemClick={(item, event) => {
|
||||||
// if the item is askAI toggle the screen
|
// if the item is askAI toggle the screen
|
||||||
if (item.type === 'askAI' && item.query) {
|
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();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { UseChatHelpers } from '@ai-sdk/react';
|
||||||
import type { AutocompleteApi, AutocompleteState, BaseItem } from '@algolia/autocomplete-core';
|
import type { AutocompleteApi, AutocompleteState, BaseItem } from '@algolia/autocomplete-core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
|
@ -14,7 +15,6 @@ import type { StartScreenTranslations } from './StartScreen';
|
||||||
import { StartScreen } from './StartScreen';
|
import { StartScreen } from './StartScreen';
|
||||||
import type { StoredSearchPlugin } from './stored-searches';
|
import type { StoredSearchPlugin } from './stored-searches';
|
||||||
import type { InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types';
|
import type { InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types';
|
||||||
import type { AskAiState } from './useAskAi';
|
|
||||||
|
|
||||||
export type ScreenStateTranslations = Partial<{
|
export type ScreenStateTranslations = Partial<{
|
||||||
errorScreen: ErrorScreenTranslations;
|
errorScreen: ErrorScreenTranslations;
|
||||||
|
|
@ -37,8 +37,9 @@ export interface ScreenStateProps<TItem extends BaseItem>
|
||||||
inputRef: React.MutableRefObject<HTMLInputElement | null>;
|
inputRef: React.MutableRefObject<HTMLInputElement | null>;
|
||||||
hitComponent: DocSearchProps['hitComponent'];
|
hitComponent: DocSearchProps['hitComponent'];
|
||||||
indexName: DocSearchProps['indexName'];
|
indexName: DocSearchProps['indexName'];
|
||||||
|
messages: UseChatHelpers['messages'];
|
||||||
|
status: UseChatHelpers['status'];
|
||||||
disableUserPersonalization: boolean;
|
disableUserPersonalization: boolean;
|
||||||
askAiState?: AskAiState;
|
|
||||||
resultsFooterComponent: DocSearchProps['resultsFooterComponent'];
|
resultsFooterComponent: DocSearchProps['resultsFooterComponent'];
|
||||||
translations: ScreenStateTranslations;
|
translations: ScreenStateTranslations;
|
||||||
getMissingResultsUrl?: DocSearchProps['getMissingResultsUrl'];
|
getMissingResultsUrl?: DocSearchProps['getMissingResultsUrl'];
|
||||||
|
|
@ -46,8 +47,15 @@ export interface ScreenStateProps<TItem extends BaseItem>
|
||||||
|
|
||||||
export const ScreenState = React.memo(
|
export const ScreenState = React.memo(
|
||||||
({ translations = {}, ...props }: ScreenStateProps<InternalDocSearchHit>) => {
|
({ translations = {}, ...props }: ScreenStateProps<InternalDocSearchHit>) => {
|
||||||
if (props.isAskAiActive && props.canHandleAskAi && props.askAiState) {
|
if (props.isAskAiActive && props.canHandleAskAi) {
|
||||||
return <AskAiScreen {...props} askAiState={props.askAiState} translations={translations?.askAiScreen} />;
|
return (
|
||||||
|
<AskAiScreen
|
||||||
|
{...props}
|
||||||
|
messages={props.messages}
|
||||||
|
status={props.status}
|
||||||
|
translations={translations?.askAiScreen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.state?.status === 'error') {
|
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';
|
import React, { type JSX } from 'react';
|
||||||
|
|
||||||
export function LoadingIcon(): JSX.Element {
|
export function LoadingIcon({ className }: { className?: string }): JSX.Element {
|
||||||
return (
|
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 fill="none" fillRule="evenodd">
|
||||||
<g transform="translate(1 1)" strokeWidth="2">
|
<g transform="translate(1 1)" strokeWidth="2">
|
||||||
<circle strokeOpacity=".3" cx="18" cy="18" r="18" />
|
<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 {
|
return {
|
||||||
add(item: TItem): void {
|
add(item: TItem): void {
|
||||||
const { askState } = item;
|
const { objectID, messages } = item;
|
||||||
|
|
||||||
// check if this query is already saved
|
// 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(
|
const isQueryAlreadySaved = items.findIndex(
|
||||||
(x) =>
|
(x) => x.objectID === objectID || x.messages?.[0]?.content === messages?.[0]?.content,
|
||||||
x.objectID === askState?.conversationId || x.askState?.messages[0].content === askState?.messages[0].content,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isQueryAlreadySaved > -1) {
|
if (isQueryAlreadySaved > -1) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { AskAiState } from '../useAskAi';
|
import type { Message } from '@ai-sdk/react';
|
||||||
|
|
||||||
import type { DocSearchHit } from './DocSearchHit';
|
import type { DocSearchHit } from './DocSearchHit';
|
||||||
|
|
||||||
export type StoredDocSearchHit = Omit<DocSearchHit, '_highlightResult' | '_snippetResult'>;
|
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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@algolia/autocomplete-core@npm:1.18.1":
|
||||||
version: 1.18.1
|
version: 1.18.1
|
||||||
resolution: "@algolia/autocomplete-core@npm:1.18.1"
|
resolution: "@algolia/autocomplete-core@npm:1.18.1"
|
||||||
|
|
@ -2099,6 +2152,7 @@ __metadata:
|
||||||
react: "npm:^19.0.0"
|
react: "npm:^19.0.0"
|
||||||
react-dom: "npm:^19.0.0"
|
react-dom: "npm:^19.0.0"
|
||||||
vite: "npm:^6.0.7"
|
vite: "npm:^6.0.7"
|
||||||
|
zod: "npm:^3.25.3"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|
@ -2140,8 +2194,9 @@ __metadata:
|
||||||
"@babel/preset-typescript": "npm:7.26.0"
|
"@babel/preset-typescript": "npm:7.26.0"
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "npm:4.4.1"
|
"@eslint-community/eslint-plugin-eslint-comments": "npm:4.4.1"
|
||||||
"@rollup/plugin-babel": "npm:6.0.4"
|
"@rollup/plugin-babel": "npm:6.0.4"
|
||||||
|
"@rollup/plugin-commonjs": "npm:^28.0.3"
|
||||||
"@rollup/plugin-json": "npm:6.1.0"
|
"@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-replace": "npm:6.0.2"
|
||||||
"@rollup/plugin-terser": "npm:0.4.4"
|
"@rollup/plugin-terser": "npm:0.4.4"
|
||||||
"@stylistic/eslint-plugin": "npm:2.13.0"
|
"@stylistic/eslint-plugin": "npm:2.13.0"
|
||||||
|
|
@ -2219,6 +2274,7 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@docsearch/react@workspace:packages/docsearch-react"
|
resolution: "@docsearch/react@workspace:packages/docsearch-react"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@ai-sdk/react": "npm:^1.2.12"
|
||||||
"@algolia/autocomplete-core": "npm:1.18.1"
|
"@algolia/autocomplete-core": "npm:1.18.1"
|
||||||
"@algolia/autocomplete-preset-algolia": "npm:1.18.1"
|
"@algolia/autocomplete-preset-algolia": "npm:1.18.1"
|
||||||
"@docsearch/css": "npm:3.9.0"
|
"@docsearch/css": "npm:3.9.0"
|
||||||
|
|
@ -4291,6 +4347,26 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@rollup/plugin-json@npm:6.1.0":
|
||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
resolution: "@rollup/plugin-json@npm:6.1.0"
|
resolution: "@rollup/plugin-json@npm:6.1.0"
|
||||||
|
|
@ -4305,9 +4381,9 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@rollup/plugin-node-resolve@npm:16.0.0":
|
"@rollup/plugin-node-resolve@npm:^16.0.1":
|
||||||
version: 16.0.0
|
version: 16.0.1
|
||||||
resolution: "@rollup/plugin-node-resolve@npm:16.0.0"
|
resolution: "@rollup/plugin-node-resolve@npm:16.0.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@rollup/pluginutils": "npm:^5.0.1"
|
"@rollup/pluginutils": "npm:^5.0.1"
|
||||||
"@types/resolve": "npm:1.20.2"
|
"@types/resolve": "npm:1.20.2"
|
||||||
|
|
@ -4319,7 +4395,7 @@ __metadata:
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
rollup:
|
rollup:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10c0/b63deb6fc14b37070ccaffacc8c10c9720f28ce7632f4fe2ee77064c0c79bcc3fe060fb77160e673c9fd847307252f25a2983030bd54f1888324063c69ae1399
|
checksum: 10c0/54d33282321492fafec29b49c66dd1efd90c72a24f9d1569dcb57a72ab8de8a782810f39fdb917b96ec6a598c18f3416588b419bf7af331793a010de1fe28c60
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -7907,6 +7983,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"compare-func@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "compare-func@npm:2.0.0"
|
resolution: "compare-func@npm:2.0.0"
|
||||||
|
|
@ -10796,6 +10879,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"feed@npm:^4.2.2":
|
||||||
version: 4.2.2
|
version: 4.2.2
|
||||||
resolution: "feed@npm:4.2.2"
|
resolution: "feed@npm:4.2.2"
|
||||||
|
|
@ -13022,6 +13117,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"is-regex@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "is-regex@npm:1.2.1"
|
resolution: "is-regex@npm:1.2.1"
|
||||||
|
|
@ -13502,7 +13606,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"json-schema@npm:0.4.0":
|
"json-schema@npm:0.4.0, json-schema@npm:^0.4.0":
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
resolution: "json-schema@npm:0.4.0"
|
resolution: "json-schema@npm:0.4.0"
|
||||||
checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3
|
checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3
|
||||||
|
|
@ -20358,6 +20462,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"select-hose@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "select-hose@npm:2.0.0"
|
resolution: "select-hose@npm:2.0.0"
|
||||||
|
|
@ -21798,6 +21909,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"symbol-tree@npm:^3.2.4":
|
||||||
version: 3.2.4
|
version: 3.2.4
|
||||||
resolution: "symbol-tree@npm:3.2.4"
|
resolution: "symbol-tree@npm:3.2.4"
|
||||||
|
|
@ -22021,6 +22144,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"throttleit@npm:^1.0.0":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "throttleit@npm:1.0.1"
|
resolution: "throttleit@npm:1.0.1"
|
||||||
|
|
@ -22839,6 +22969,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
resolution: "util-deprecate@npm:1.0.2"
|
resolution: "util-deprecate@npm:1.0.2"
|
||||||
|
|
@ -23859,6 +23998,22 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"zwitch@npm:^2.0.0":
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
resolution: "zwitch@npm:2.0.4"
|
resolution: "zwitch@npm:2.0.4"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue