mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 16:02:36 +00:00
* 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>
73 lines
3.0 KiB
TypeScript
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]
|
|
)
|
|
}
|