@tanstack/ai-preact 0.14.4 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ import { useMemo, useRef } from 'preact/compat'
2
+ import type { ComponentType } from 'preact/compat'
3
+ import type { InferredClientContext } from '@tanstack/ai-client'
4
+ import type { ChatUIInterruptsOf, ChatUIToolsOf } from '@tanstack/ai-client/ui'
5
+ import { useChat as useUnboundChat } from '../use-chat'
6
+ import type { UseChatOptions } from '../types'
7
+ import { createChatUI } from './create-ui'
8
+ import type { ChatUIFactoryConfig, ChatUIHost, InputProps } from './create-ui'
9
+
10
+ type HeadlessOptions<TOptions> = UseChatOptions<
11
+ ChatUIToolsOf<TOptions>,
12
+ InferredClientContext<ChatUIToolsOf<TOptions>>,
13
+ ChatUIInterruptsOf<TOptions>
14
+ >
15
+
16
+ type ChatInstanceOverrides<TOptions> = {
17
+ threadId?: string
18
+ live?: boolean
19
+ forwardedProps?: Record<string, any>
20
+ body?: Record<string, any>
21
+ initialMessages?: HeadlessOptions<TOptions>['initialMessages']
22
+ }
23
+
24
+ /**
25
+ * Bind chat options and UI widgets once at module scope.
26
+ *
27
+ * This matches Form `createFormHook` and Table `createTableHook`. The
28
+ * returned `useAppChat` creates a chat instance. `chat.AppChat` renders it.
29
+ * `useChatContext` reads that instance. Part and interrupt widgets take
30
+ * `part` / `interrupt` as props. Do not read those from context.
31
+ *
32
+ * Pass instance overrides such as `threadId` into `useAppChat()`.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * const { useAppChat, useChatContext } = createChatHook({
37
+ * options: chatOptions,
38
+ * components: { layout: ChatLayout, message: ChatMessage },
39
+ * partsComponents: { fallback: FallbackPart },
40
+ * toolsComponents: { getWeather: WeatherTool, purchaseItem: PurchaseTool },
41
+ * interruptsComponents: { generic: { choosePlan: ChoosePlan } },
42
+ * })
43
+ *
44
+ * function Support() {
45
+ * const chat = useAppChat({ threadId: 'support-1' })
46
+ * return <chat.AppChat />
47
+ * }
48
+ * ```
49
+ */
50
+ export function createChatHook<
51
+ const TOptions,
52
+ TInput extends ComponentType<any> | undefined =
53
+ | ComponentType<InputProps<NoInfer<TOptions>>>
54
+ | undefined,
55
+ >({
56
+ options,
57
+ ...chatComponents
58
+ }: {
59
+ options: TOptions
60
+ } & ChatUIFactoryConfig<NoInfer<TOptions>, TInput>) {
61
+ const ui = createChatUI(
62
+ options,
63
+ chatComponents as ChatUIFactoryConfig<NoInfer<TOptions>, TInput>,
64
+ )
65
+
66
+ function useAppChat(overrides?: ChatInstanceOverrides<TOptions>) {
67
+ const chat = (
68
+ overrides
69
+ ? useUnboundChat({
70
+ ...(options as HeadlessOptions<TOptions>),
71
+ ...overrides,
72
+ })
73
+ : useUnboundChat(options as HeadlessOptions<TOptions>)
74
+ ) as ChatUIHost<TOptions>
75
+
76
+ // `useChat` returns a fresh object on every render. The mixed Chat
77
+ // component must stay stable, or React remounts the tree on each
78
+ // update. Read the current instance from a ref.
79
+ const chatRef = useRef(chat)
80
+ chatRef.current = chat
81
+
82
+ const AppChat = useMemo(() => {
83
+ function AppChatImpl() {
84
+ return <ui.Chat chat={chatRef.current} />
85
+ }
86
+ return AppChatImpl
87
+ }, [])
88
+
89
+ return Object.assign(chat, { AppChat }) as ChatUIHost<TOptions> & {
90
+ AppChat: ComponentType
91
+ }
92
+ }
93
+
94
+ return {
95
+ useAppChat,
96
+ useChatContext: ui.useChatContext,
97
+ }
98
+ }
@@ -0,0 +1,57 @@
1
+ import { createContext, useContext } from 'preact/compat'
2
+ import type { Context } from 'preact/compat'
3
+ import type {
4
+ ChatUIInterrupt,
5
+ ChatUISelectedPart,
6
+ } from '@tanstack/ai-client/ui'
7
+ import type { UseChatReturn } from '../types'
8
+
9
+ /**
10
+ * Scoped chat, part, and interrupt contexts plus matching context hooks.
11
+ * Pass these into {@link createChatHook} when widgets live in other files, or
12
+ * when two chat trees nest and must not share the default contexts.
13
+ */
14
+ export interface ChatUIContexts {
15
+ chatContext: Context<UseChatReturn<any, any> | null>
16
+ partContext: Context<ChatUISelectedPart | null>
17
+ interruptContext: Context<ChatUIInterrupt | null>
18
+ useChatContext: () => UseChatReturn<any, any>
19
+ }
20
+
21
+ /**
22
+ * Create a fresh set of chat UI contexts. This matches Form
23
+ * `createFormHookContexts` and Table `createTableHookContexts`.
24
+ *
25
+ * Most apps can skip this. `createChatHook` uses shared module contexts by
26
+ * default. The hooks on this object are the escape hatch for a widget file
27
+ * that cannot import the `createChatHook` result (circular import), or for
28
+ * nested chats that need isolated providers.
29
+ *
30
+ * Prefer the `useChatContext` hook returned from `createChatHook` when you
31
+ * can import that result. That hook knows the `chatComponents` types. Part
32
+ * and interrupt widgets take `part` / `interrupt` as props.
33
+ */
34
+ export function createChatHookContexts(): ChatUIContexts {
35
+ const chatContext = createContext<UseChatReturn<any, any> | null>(null)
36
+ const partContext = createContext<ChatUISelectedPart | null>(null)
37
+ const interruptContext = createContext<ChatUIInterrupt | null>(null)
38
+
39
+ function useChatContext() {
40
+ const chat = useContext(chatContext)
41
+ if (!chat) {
42
+ throw new Error(
43
+ '`useChatContext` must be used within `UI.Provider` or `UI.Chat`.',
44
+ )
45
+ }
46
+ return chat
47
+ }
48
+
49
+ return {
50
+ chatContext,
51
+ partContext,
52
+ interruptContext,
53
+ useChatContext,
54
+ }
55
+ }
56
+
57
+ export const defaultChatUIContexts = createChatHookContexts()