Files
windmill/chat-sdk/src/react.ts
T
Ruben FiszelandClaude Fable 5.1 8b4f6220dc refactor: run the flow chat UI on the windmill-chat sdk (#11134)
* refactor: run the flow chat UI on the windmill-chat sdk

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: structural answer check, poll option and latest run in the chat sdk

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: count jobless tool rows and the loaded license in the flow chat

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 12:56:07 +00:00

73 lines
3.0 KiB
TypeScript

import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
import { createChat } from './chat'
import type { Chat, ChatOptions, ChatState } from './types'
export type UseWindmillChat = ChatState &
Pick<
Chat,
| 'sendMessage'
| 'stop'
| 'newConversation'
| 'selectConversation'
| 'loadConversations'
| 'deleteConversation'
| 'loadOlderMessages'
> & { chat: Chat }
/**
* A chat on a chat-mode flow. The chat is created once per `flowPath`, `baseUrl`,
* `workspace`, `history`, `storageKey`, credential and presence of `run`, and
* destroyed on unmount. A credential change is a new user, whose chat must not
* carry the previous one's state: a different token string, or a switch between no
* token, a token string and a token function, all recreate it. A token function is
* read through a ref on every call, so a new closure per render changes what the
* next call runs and nothing else; pass a `storageKey` per user when local history
* must not be shared. `run`, the callbacks and `inputs` are read the same way: the
* latest render's values go with the next message.
*/
export function useWindmillChat(options: ChatOptions): UseWindmillChat {
const latest = useRef(options)
latest.current = options
const credential =
typeof options.token === 'function' ? 'fn' : typeof options.token === 'string' ? `str:${options.token}` : 'none'
// A custom runner replaces the deployed flow call, so its presence is part of what the chat is.
const customRun = options.run !== undefined
const chat = useMemo(
() =>
createChat({
...options,
// Sent per message from the latest render instead, so a removed key stays removed.
inputs: undefined,
token:
typeof options.token === 'function'
? () => {
const token = latest.current.token
return typeof token === 'function' ? token() : (token ?? '')
}
: options.token,
run: customRun ? (args, turn) => (latest.current.run ?? options.run!)(args, turn) : undefined,
onFinish: (turn) => latest.current.onFinish?.(turn),
onError: (error, turn) => latest.current.onError?.(error, turn)
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[options.flowPath, options.baseUrl, options.workspace, options.history, options.storageKey, credential, customRun]
)
useEffect(() => () => chat.destroy(), [chat])
const state = useSyncExternalStore(chat.subscribe, chat.getState, chat.getState)
return useMemo(
() => ({
...state,
chat,
sendMessage: (text, options) =>
chat.sendMessage(text, { ...options, inputs: { ...latest.current.inputs, ...options?.inputs } }),
stop: chat.stop,
newConversation: chat.newConversation,
selectConversation: chat.selectConversation,
loadConversations: chat.loadConversations,
deleteConversation: chat.deleteConversation,
loadOlderMessages: chat.loadOlderMessages
}),
[state, chat]
)
}