import type { ChatRecordsStore } from '../../lib/utils/chatRecords'; import type { ComponentProps, SendEventForHits } from '../../types'; import type { SearchParameters } from 'algoliasearch-helper'; export type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error'; export type ChatRole = 'data' | 'user' | 'assistant' | 'system'; /** * Provider metadata type for UI message parts. */ export type ProviderMetadata = Record>; /** * A record of data types for data parts in UI messages. */ export type UIDataTypes = Record; /** * Tool input/output type definition. */ export type UITool = { input: unknown; output: unknown | undefined; }; /** * A record of UI tools. */ export type UITools = Record; /** * Helper type to get values of an object. */ type ValueOf = T[keyof T]; /** * Deep partial type. */ type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; } : T; /** * A text part of a message. */ export type TextUIPart = { type: 'text'; text: string; state?: 'streaming' | 'done'; providerMetadata?: ProviderMetadata; }; /** * A reasoning part of a message. */ export type ReasoningUIPart = { type: 'reasoning'; text: string; state?: 'streaming' | 'done'; providerMetadata?: ProviderMetadata; }; /** * A source URL part of a message. */ export type SourceUrlUIPart = { type: 'source-url'; sourceId: string; url: string; title?: string; providerMetadata?: ProviderMetadata; }; /** * A document source part of a message. */ export type SourceDocumentUIPart = { type: 'source-document'; sourceId: string; mediaType: string; title: string; filename?: string; providerMetadata?: ProviderMetadata; }; /** * A file part of a message. */ export type FileUIPart = { type: 'file'; mediaType: string; filename?: string; url: string; providerMetadata?: ProviderMetadata; }; /** * A step boundary part of a message. */ export type StepStartUIPart = { type: 'step-start'; }; /** * A data part of a message. */ export type DataUIPart = ValueOf<{ [NAME in keyof TDataTypes & string]: { type: `data-${NAME}`; id?: string; data: TDataTypes[NAME]; }; }>; /** * A tool invocation part of a message. */ export type ToolUIPart = ValueOf<{ [NAME in keyof TTools & string]: { type: `tool-${NAME}`; toolCallId: string; } & ({ state: 'input-streaming'; input: DeepPartial | undefined; /** * The raw accumulated input text. `input` is parsed from it with * partial-JSON repair, so a value still mid-delta can be exposed as a * complete one. Consult this when completeness matters. */ rawInput?: string; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: 'input-available'; input: TTools[NAME]['input']; providerExecuted?: boolean; output?: never; errorText?: never; callProviderMetadata?: ProviderMetadata; } | { state: 'output-available'; input: TTools[NAME]['input']; output: TTools[NAME]['output']; errorText?: never; providerExecuted?: boolean; callProviderMetadata?: ProviderMetadata; preliminary?: boolean; } | { state: 'output-error'; input: TTools[NAME]['input'] | undefined; rawInput?: unknown; output?: never; errorText: string; providerExecuted?: boolean; callProviderMetadata?: ProviderMetadata; }); }>; /** * A dynamic tool invocation part of a message. */ export type DynamicToolUIPart = { type: 'dynamic-tool'; toolName: string; toolCallId: string; } & ({ state: 'input-streaming'; input: unknown | undefined; output?: never; errorText?: never; } | { state: 'input-available'; input: unknown; output?: never; errorText?: never; callProviderMetadata?: ProviderMetadata; } | { state: 'output-available'; input: unknown; output: unknown; errorText?: never; callProviderMetadata?: ProviderMetadata; preliminary?: boolean; } | { state: 'output-error'; input: unknown; output?: never; errorText: string; callProviderMetadata?: ProviderMetadata; }); /** * All possible message part types. */ export type UIMessagePart = TextUIPart | ReasoningUIPart | ToolUIPart | DynamicToolUIPart | SourceUrlUIPart | SourceDocumentUIPart | FileUIPart | DataUIPart | StepStartUIPart; /** * AI SDK UI Messages. They are used in the client and to communicate between the frontend and the API routes. */ export interface UIMessage { /** A unique identifier for the message. */ id: string; /** The role of the message. */ role: 'system' | 'user' | 'assistant'; /** The metadata of the message. */ metadata?: TMetadata; /** The parts of the message. Use this for rendering the message in the UI. */ parts: Array>; } export type ChatMessageBase = UIMessage; export type ChatToolMessage = Extract; export type ChatToolType = ChatToolMessage['type']; /** * Infer metadata type from UIMessage. */ export type InferUIMessageMetadata = T extends UIMessage ? TMetadata : unknown; /** * Infer data types from UIMessage. */ export type InferUIMessageData = T extends UIMessage ? TDataTypes : UIDataTypes; /** * Infer tools from UIMessage. */ export type InferUIMessageTools = T extends UIMessage ? TTools : UITools; /** * Chat state interface. */ export interface ChatState { status: ChatStatus; error: Error | undefined; messages: TUIMessage[]; pushMessage: (message: TUIMessage) => void; popMessage: () => void; replaceMessage: (index: number, message: TUIMessage) => void; snapshot: (thing: T) => T; } /** * ID generator function type. */ export type IdGenerator = () => string; /** * Callback function to be called when an error is encountered. */ export type ChatOnErrorCallback = (error: Error) => void; /** * Infer tool call type from UIMessage. */ export type InferUIMessageToolCall = ValueOf<{ [NAME in keyof InferUIMessageTools]: { toolName: NAME & string; toolCallId: string; input: InferUIMessageTools[NAME] extends { input: infer INPUT; } ? INPUT : never; dynamic?: false; }; }> | { toolName: string; toolCallId: string; input: unknown; dynamic: true; }; /** * Optional callback function that is invoked when a tool call is received. */ export type ChatOnToolCallCallback = (options: { toolCall: InferUIMessageToolCall; signal: AbortSignal; }) => void | PromiseLike; /** * Function that is called when the assistant response has finished streaming. */ export type ChatOnFinishCallback = (options: { message: TUIMessage; messages: TUIMessage[]; isAbort: boolean; isDisconnect: boolean; isError: boolean; }) => void; /** * Optional callback function that is called when a data part is received. */ export type ChatOnDataCallback = (dataPart: DataUIPart>) => void; /** * Transport interface for sending and receiving chat messages. */ export interface ChatTransport { sendMessages: (options: { chatId: string; messages: TUIMessage[]; abortSignal: AbortSignal; requestMetadata?: unknown; trigger: 'submit-message' | 'regenerate-message'; messageId?: string; }) => Promise>; reconnectToStream: (options: { chatId: string; }) => Promise | null>; } /** * Chat initialization options. */ export interface ChatInit { /** A unique identifier for the chat. If not provided, a random one will be generated. */ id?: string; messages?: TUIMessage[]; /** A way to provide a function for generating message and chat IDs. */ generateId?: IdGenerator; transport?: ChatTransport; /** Callback function to be called when an error is encountered. */ onError?: ChatOnErrorCallback; /** Optional callback function that is invoked when a tool call is received. */ onToolCall?: ChatOnToolCallCallback; /** Function that is called when the assistant response has finished streaming. */ onFinish?: ChatOnFinishCallback; /** Optional callback function that is called when a data part is received. */ onData?: ChatOnDataCallback; /** * When provided, this function will be called when the stream is finished or a tool call is added * to determine if the current messages should be resubmitted. */ sendAutomaticallyWhen?: (options: { messages: TUIMessage[]; }) => boolean | PromiseLike; } /** * Abstract base class for chat implementations. */ export interface AbstractChat { readonly id: string; readonly generateId: IdGenerator; status: ChatStatus; error: Error | undefined; messages: TUIMessage[]; lastMessage: TUIMessage | undefined; sendMessage: (message?: (Omit & { id?: TUIMessage['id']; role?: TUIMessage['role']; text?: never; files?: never; messageId?: string; }) | { text: string; files?: FileList | FileUIPart[]; metadata?: InferUIMessageMetadata; parts?: never; messageId?: string; } | { files: FileList | FileUIPart[]; metadata?: InferUIMessageMetadata; parts?: never; messageId?: string; }, options?: { headers?: Record | Headers; body?: object; }) => Promise; regenerate: (options?: { messageId?: string; } & { headers?: Record | Headers; body?: object; }) => Promise; resumeStream: (options?: { headers?: Record | Headers; body?: object; }) => Promise; resetConversationId: () => void; clearError: () => void; addToolResult: >(params: { tool: TTool; toolCallId: string; } & ({ state?: 'output-available'; output: InferUIMessageTools[TTool]['output']; errorText?: never; } | { state: 'output-error'; output?: never; errorText: string; })) => Promise; stop: () => Promise; } export type AddToolResult = AbstractChat['addToolResult']; export type AddToolResultWithOutput = (params: { output: unknown; }) => ReturnType; export type AddToolResultForToolCall = (params: { state?: 'output-available'; output: unknown; errorText?: never; } | { state: 'output-error'; output?: never; errorText: string; }) => ReturnType; type SearchToolExtraFields = { [key: string]: unknown; }; type SearchToolQueryBase = SearchToolExtraFields & { query: string; number_of_results?: number; }; type FacetFiltersSearchToolQuery = SearchToolQueryBase & { facet_filters?: string[][]; }; type FacetKeysSearchToolQuery = SearchToolQueryBase & { facet_filters?: undefined; [facetKey: `facet_${string}`]: string[] | boolean | undefined; }; /** * A single query of a search tool input: the query string along with its * refinements, either as a ready-to-use `facet_filters` array or as individual * `facet_` keys. */ export type SearchToolQuery = FacetFiltersSearchToolQuery | FacetKeysSearchToolQuery; /** Search tool input holding the query and its refinements at the root. */ type SingleQuerySearchToolInput = SearchToolQuery & { queries?: undefined; }; /** Search tool input nesting one or more queries in a `queries` array. */ type MultiQuerySearchToolInput = SearchToolExtraFields & { query?: undefined; facet_filters?: undefined; queries: SearchToolQuery[]; }; export type SearchToolInput = SingleQuerySearchToolInput | MultiQuerySearchToolInput; export type ApplyFiltersParams = { query?: string; facetFilters?: string[][]; /** * Numeric refinements, in the Algolia `numericFilters` format * (e.g. `['price <= 1500']`). Only the search tool's resolved search params * can express these; the raw `facet_` keys cannot. */ numericFilters?: string[]; }; /** * The search parameters a search tool call was actually answered with, as the * Algolia MCP Server resolved them: after defaults, clamping and the * allow-list, and including parameters the model never sent. * * Read with `getResolvedSearchParams`. Absent whenever the server emits no * `_meta` for the tool result. */ export type ResolvedSearchParams = { query?: string; facetFilters?: string[][]; numericFilters?: string[]; }; export type ChatLayoutOwnProps = { open: boolean; maximized: boolean; headerComponent: JSX.Element; messagesComponent: JSX.Element; promptComponent: JSX.Element; classNames?: { root?: string | string[]; container?: string | string[]; }; isClearing?: boolean; clearMessages?: () => void; onClearTransitionEnd?: () => void; suggestions?: string[]; tools: ClientSideTools; } & Pick, 'messages'> & Partial, 'status'>> & Pick, 'sendMessage' | 'regenerate' | 'stop' | 'error'> & ComponentProps<'div'>; /** * Where the chat loader renders: as its own row after the last message * (`messages-end`, the default), or inside the streaming assistant message * (`message-inline`, falling back to a row when there is none to host it). */ export type ChatLoaderPosition = 'messages-end' | 'message-inline'; /** * What the turn is doing while the loader shows: the request is `submitted` with * nothing back yet, a `tool` call is in flight, `reasoning` settled before the * answer started, or `thinking` for anything else. */ export type ChatLoaderPhase = 'submitted' | 'tool' | 'reasoning' | 'thinking'; /** * Shared chat state and callbacks injected into every overridable chat * component by the widget. This is the component-layer analog of the templates * system's `params` argument: a single, consistent object every component can * read, regardless of which override point it plugs into. */ export type ChatComponentContext = { /** * The messages currently in the chat. */ messages: TMessage[]; /** * Current chat status. */ status: ChatStatus; /** * The current error, when the chat is in an error state. */ error?: Error; /** * Whether the messages are being cleared (drives the clearing animation). */ isClearing: boolean; /** * Whether the chat panel is open. */ open: boolean; /** * Whether the chat panel is maximized. */ maximized: boolean; /** * The message part currently being processed by the assistant, if any. */ activePart?: TMessage['parts'][number]; /** * Tools registered for the assistant. */ tools: ClientSideTools; /** * Send a message to the chat. */ sendMessage?: ChatLayoutOwnProps['sendMessage']; /** * Regenerate the last assistant response. */ regenerate: ChatLayoutOwnProps['regenerate']; /** * Stop the current streaming response. */ stop: ChatLayoutOwnProps['stop']; /** * Set the prompt input value. */ setInput?: (input: string) => void; /** * Reload (regenerate) a message, optionally targeting a specific message id. */ onReload: (messageId?: string) => void; /** * Clear the conversation and start a new one, when available. */ onNewConversation?: () => void; /** * Close the chat. */ onClose: () => void; }; /** * The `context` the loader receives: the shared `ChatComponentContext` plus what * the current turn is doing. */ export type ChatLoaderContext = ChatComponentContext & { /** * What the turn is doing right now. */ phase: ChatLoaderPhase; /** * The message the loader belongs to, when there is one. */ message?: TMessage; }; /** * Augments a chat component's own props with the shared `context` the widget * always injects. `TOwnProps` is the per-component presentational config that * stays at the root; `context` is the shared chat state and callbacks. */ export type ChatComponentPropsWithContext = TOwnProps & { context: ChatComponentContext; }; /** * The `context` a tool layout component receives: the shared * `ChatComponentContext` merged with the tool's own injected data (the tool * `message`, event/filter callbacks, and index UI state). */ export type ClientSideToolContext = ChatComponentContext & { message: ChatToolMessage; /** * The records the chat's tools have fetched. A tool handed plain object IDs * hydrates them with `records.get(objectID)`. */ records?: ChatRecordsStore; insightsEventContext?: ChatInsightsEventContext; indexUiState: object; setIndexUiState: (state: object) => void; addToolResult: AddToolResultWithOutput; applyFilters: (params: ApplyFiltersParams) => SearchParameters; sendEvent: SendEventForHits; }; /** * The root-level props tool layout components received before everything moved * under `context`. Still passed alongside `context` so components written * against the previous API keep working; they are removed in the next major. * * These are spelled out rather than derived with `Pick` so that `@deprecated` * reaches each property at the point of use in editors. */ type DeprecatedClientSideToolRootProps = { /** @deprecated Read `context.message` instead. */ message: ChatToolMessage; /** @deprecated Read `context.messages` instead. */ messages: TMessage[]; /** @deprecated Read `context.records` instead. */ records?: ChatRecordsStore; /** @deprecated Read `context.insightsEventContext` instead. */ insightsEventContext?: ChatInsightsEventContext; /** @deprecated Read `context.status` instead. */ status: ChatStatus; /** @deprecated Read `context.indexUiState` instead. */ indexUiState: object; /** @deprecated Read `context.setIndexUiState` instead. */ setIndexUiState: (state: object) => void; /** @deprecated Read `context.onClose` instead. */ onClose: () => void; /** @deprecated Read `context.addToolResult` instead. */ addToolResult: AddToolResultWithOutput; /** @deprecated Read `context.applyFilters` instead. */ applyFilters: (params: ApplyFiltersParams) => SearchParameters; /** @deprecated Read `context.sendEvent` instead. */ sendEvent: SendEventForHits; }; /** * Tool layout components receive a single `context` object holding everything * they render from. The deprecated root-level props are kept required (rather * than optional) so existing components that destructure them keep * type-checking under `strict`; the widget always supplies both. */ export type ClientSideToolComponentProps = { context: ClientSideToolContext; } & DeprecatedClientSideToolRootProps; export type ClientSideToolComponent = (props: ClientSideToolComponentProps) => JSX.Element; export type ChatInsightsEventContext = { agentId?: string; instantSearchStatus?: 'idle' | 'loading' | 'stalled' | 'error'; }; /** * The `context` a tool's `shouldRender` predicate receives: the shared * `ChatComponentContext`, the tool part under consideration, and the chat * message that part belongs to. * * Narrower than `ClientSideToolContext` on purpose. The predicate decides * whether anything renders at all, and it also runs from the loader, which has * none of the render-time callbacks a layout component is handed. */ export type ClientSideToolShouldRenderContext = ChatComponentContext & { /** * The tool part being considered for rendering. */ message: ChatToolMessage; /** * The chat message the tool part belongs to. */ parentMessage: TMessage; }; export type ClientSideTool = { layoutComponent?: ClientSideToolComponent; streamInput?: boolean; /** * Whether this tool also handles a call sent under `toolName`. * * Consulted only when no tool is registered under that exact name, so it * can't shadow another registration. Needed when the server names a call * after the registered tool: the Algolia MCP Server appends the index name, * so `algolia_search_index` has to answer to `algolia_search_index_products`. * * Omitted means the tool only handles its own name. */ matchesToolName?: (toolName: string) => boolean; /** * Whether this tool call should render. * * Returning `false` skips the part entirely and keeps the loader visible, so * a tool can defer to another one that renders the same turn — for example a * search tool stepping aside for a richer display tool. Omitted means always * render. */ shouldRender?: (context: ClientSideToolShouldRenderContext) => boolean; addToolResult: AddToolResult; /** Attached by the connector, one per chat; reaches `layoutComponent`. */ records?: ChatRecordsStore; sendEvent?: SendEventForHits; insightsEventContext?: ChatInsightsEventContext; onToolCall?: (params: Parameters['onToolCall']>>[0]['toolCall'] & { addToolResult: AddToolResultForToolCall; signal: AbortSignal; }) => void | PromiseLike; /** * Maximum time in milliseconds for `onToolCall` to submit a result. * Set to `false` to disable the timeout. * * @default 20000 */ timeout?: number | false; /** * Whether the default failed state shows a retry action. * * Retrying regenerates the assistant response and may execute the tool * again. Enable this only when repeating the operation is safe. Custom * layouts can implement recovery with `context.onReload`. * * @default false */ retryOnError?: boolean; /** * Output reported for this tool call when a request is sent while it is still * waiting for a result, for example `{ confirmed: false }` for a confirmation * prompt. Without it, the call is reported as failed. * * This only affects what is sent: the tool keeps waiting locally, so a result * submitted later still lands. */ cancelOutput?: (params: { toolCallId: string; input: unknown; }) => unknown; applyFilters: (params: ApplyFiltersParams) => SearchParameters; }; export type ClientSideTools = Record; export type UserClientSideTool = Omit; export type UserClientSideTools = Record; /** * @deprecated Use `ChatComponentPropsWithContext` instead — an empty/greeting * component now reads shared chat state from its `context` prop. */ export type ChatEmptyProps = Partial>; export {};