@tanstack/ai-solid 0.19.3 → 0.20.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.
- package/dist/create-chat-hook.d.ts +39 -0
- package/dist/create-chat-hook.js +38 -0
- package/dist/create-chat-hook.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +16 -4
- package/src/chat-ui/chat-input.tsx +168 -0
- package/src/chat-ui/chat-message.tsx +261 -0
- package/src/chat-ui/chat-messages.tsx +91 -0
- package/src/chat-ui/chat.tsx +95 -0
- package/src/chat-ui/create-chat-hook.tsx +77 -0
- package/src/chat-ui/create-ui-contexts.tsx +52 -0
- package/src/chat-ui/create-ui.tsx +718 -0
- package/src/chat-ui/markdown-plugins.ts +48 -0
- package/src/chat-ui/text-part.tsx +71 -0
- package/src/chat-ui/thinking-part.tsx +79 -0
- package/src/chat-ui/tool-approval.tsx +121 -0
- package/src/create-chat-hook.ts +67 -0
- package/src/index.ts +1 -0
- package/src/ui.ts +39 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { UseChatOptions, UseChatReturn } from "./types.js";
|
|
2
|
+
import { InferredClientContext } from "@tanstack/ai-client";
|
|
3
|
+
import { AnyClientTool, InterruptDefinition, SchemaInput } from "@tanstack/ai";
|
|
4
|
+
|
|
5
|
+
//#region src/create-chat-hook.d.ts
|
|
6
|
+
type ChatHookOverrides<TTools extends ReadonlyArray<AnyClientTool>, TSchema extends SchemaInput | undefined, TContext, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>> = {
|
|
7
|
+
threadId?: string;
|
|
8
|
+
live?: boolean;
|
|
9
|
+
forwardedProps?: Record<string, any>;
|
|
10
|
+
body?: Record<string, any>;
|
|
11
|
+
initialMessages?: UseChatOptions<TTools, TSchema, TContext, TInterrupts>['initialMessages'];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Bind chat options once at module scope. The returned `useChat` primitive
|
|
15
|
+
* creates a chat instance from those options.
|
|
16
|
+
*
|
|
17
|
+
* Pass per-call overrides for instance keys such as `threadId`,
|
|
18
|
+
* `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
|
|
19
|
+
* `interrupts`, or `outputSchema` here. Those stay on the factory options.
|
|
20
|
+
*
|
|
21
|
+
* Rename the primitive at the call site if you already import `useChat` from
|
|
22
|
+
* `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* const { useChat } = createChatHook(chatOptions)
|
|
27
|
+
*
|
|
28
|
+
* function Support() {
|
|
29
|
+
* const chat = useChat({ threadId: 'support-1' })
|
|
30
|
+
* return <UI.Chat chat={chat} components={components} />
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare function createChatHook<const TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>, const TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>): {
|
|
35
|
+
useChat: (overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>) => UseChatReturn<TTools, TSchema, TInterrupts>;
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { createChatHook };
|
|
39
|
+
//# sourceMappingURL=create-chat-hook.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useChat } from "./use-chat.js";
|
|
2
|
+
|
|
3
|
+
//#region src/create-chat-hook.ts
|
|
4
|
+
/**
|
|
5
|
+
* Bind chat options once at module scope. The returned `useChat` primitive
|
|
6
|
+
* creates a chat instance from those options.
|
|
7
|
+
*
|
|
8
|
+
* Pass per-call overrides for instance keys such as `threadId`,
|
|
9
|
+
* `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
|
|
10
|
+
* `interrupts`, or `outputSchema` here. Those stay on the factory options.
|
|
11
|
+
*
|
|
12
|
+
* Rename the primitive at the call site if you already import `useChat` from
|
|
13
|
+
* `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const { useChat } = createChatHook(chatOptions)
|
|
18
|
+
*
|
|
19
|
+
* function Support() {
|
|
20
|
+
* const chat = useChat({ threadId: 'support-1' })
|
|
21
|
+
* return <UI.Chat chat={chat} components={components} />
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function createChatHook(options) {
|
|
26
|
+
function useChat$1(overrides) {
|
|
27
|
+
if (!overrides) return useChat(options);
|
|
28
|
+
return useChat({
|
|
29
|
+
...options,
|
|
30
|
+
...overrides
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return { useChat: useChat$1 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { createChatHook };
|
|
38
|
+
//# sourceMappingURL=create-chat-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-chat-hook.js","names":["useChat","useUnboundChat"],"sources":["../src/create-chat-hook.ts"],"sourcesContent":["import { useChat as useUnboundChat } from './use-chat'\nimport type {\n AnyClientTool,\n InterruptDefinition,\n SchemaInput,\n} from '@tanstack/ai'\nimport type { InferredClientContext } from '@tanstack/ai-client'\nimport type { UseChatOptions } from './types'\n\ntype ChatHookOverrides<\n TTools extends ReadonlyArray<AnyClientTool>,\n TSchema extends SchemaInput | undefined,\n TContext,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,\n> = {\n threadId?: string\n live?: boolean\n forwardedProps?: Record<string, any>\n body?: Record<string, any>\n initialMessages?: UseChatOptions<\n TTools,\n TSchema,\n TContext,\n TInterrupts\n >['initialMessages']\n}\n\n/**\n * Bind chat options once at module scope. The returned `useChat` primitive\n * creates a chat instance from those options.\n *\n * Pass per-call overrides for instance keys such as `threadId`,\n * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,\n * `interrupts`, or `outputSchema` here. Those stay on the factory options.\n *\n * Rename the primitive at the call site if you already import `useChat` from\n * `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.\n *\n * @example\n * ```tsx\n * const { useChat } = createChatHook(chatOptions)\n *\n * function Support() {\n * const chat = useChat({ threadId: 'support-1' })\n * return <UI.Chat chat={chat} components={components} />\n * }\n * ```\n */\nexport function createChatHook<\n const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>) {\n function useChat(\n overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>,\n ) {\n if (!overrides) {\n return useUnboundChat(options)\n }\n return useUnboundChat({ ...options, ...overrides })\n }\n\n return { useChat }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,eAOd,SAAiE;CACjE,SAASA,UACP,WACA;AACA,MAAI,CAAC,UACH,QAAOC,QAAe,QAAQ;AAEhC,SAAOA,QAAe;GAAE,GAAG;GAAS,GAAG;GAAW,CAAC;;AAGrD,QAAO,EAAE,oBAAS"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChatRequestBody, DeepPartial, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, UseChatOptions, UseChatReturn, WhenBusy } from "./types.js";
|
|
2
2
|
import { useChat } from "./use-chat.js";
|
|
3
|
+
import { createChatHook } from "./create-chat-hook.js";
|
|
3
4
|
import { useByok } from "./use-byok.js";
|
|
4
5
|
import { UseGenerationOptions, UseGenerationReturn, useGeneration } from "./use-generation.js";
|
|
5
6
|
import { UseGenerateImageOptions, UseGenerateImageReturn, useGenerateImage } from "./use-generate-image.js";
|
|
@@ -10,4 +11,4 @@ import { UseSummarizeOptions, UseSummarizeReturn, useSummarize } from "./use-sum
|
|
|
10
11
|
import { UseGenerateVideoOptions, UseGenerateVideoReturn, useGenerateVideo } from "./use-generate-video.js";
|
|
11
12
|
import { UseAudioRecorderOptions, UseAudioRecorderReturn, useAudioRecorder } from "./use-audio-recorder.js";
|
|
12
13
|
import { AudioGenerateInput, ChatClientPersistence, ChatPersistedState, ChatPersistenceOption, ChatStorageAdapter, ConnectConnectionAdapter, ConnectionAdapter, FetchConnectionOptions, GenerationClientState, ImageGenerateInput, IndexedDBPersistenceOptions, InferChatMessages, RunAgentInputContext, SpeechGenerateInput, StorageUnavailableError, SubscribeConnectionAdapter, SummarizeGenerateInput, TranscriptionGenerateInput, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo, WebSocketConnectionOptions, WebStoragePersistenceOptions, XhrConnectionOptions, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
|
|
13
|
-
export { type AudioGenerateInput, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatRequestBody, type ChatStorageAdapter, type ConnectConnectionAdapter, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type IndexedDBPersistenceOptions, type InferChatMessages, type QueueConfig, type QueueOption, type QueueStrategy, type QueuedMessage, type RunAgentInputContext, type SendMessageOptions, type SpeechGenerateInput, StorageUnavailableError, type SubscribeConnectionAdapter, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseAudioRecorderOptions, type UseAudioRecorderReturn, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, type WebSocketConnectionOptions, type WebStoragePersistenceOptions, type WhenBusy, type XhrConnectionOptions, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
|
|
14
|
+
export { type AudioGenerateInput, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatRequestBody, type ChatStorageAdapter, type ConnectConnectionAdapter, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type IndexedDBPersistenceOptions, type InferChatMessages, type QueueConfig, type QueueOption, type QueueStrategy, type QueuedMessage, type RunAgentInputContext, type SendMessageOptions, type SpeechGenerateInput, StorageUnavailableError, type SubscribeConnectionAdapter, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseAudioRecorderOptions, type UseAudioRecorderReturn, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, type WebSocketConnectionOptions, type WebStoragePersistenceOptions, type WhenBusy, type XhrConnectionOptions, createChatClientOptions, createChatHook, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useChat } from "./use-chat.js";
|
|
2
|
+
import { createChatHook } from "./create-chat-hook.js";
|
|
2
3
|
import { useByok } from "./use-byok.js";
|
|
3
4
|
import { useGeneration } from "./use-generation.js";
|
|
4
5
|
import { useGenerateImage } from "./use-generate-image.js";
|
|
@@ -10,4 +11,4 @@ import { useGenerateVideo } from "./use-generate-video.js";
|
|
|
10
11
|
import { useAudioRecorder } from "./use-audio-recorder.js";
|
|
11
12
|
import { StorageUnavailableError, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
|
|
12
13
|
|
|
13
|
-
export { StorageUnavailableError, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
|
|
14
|
+
export { StorageUnavailableError, createChatClientOptions, createChatHook, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Solid hooks for TanStack AI streaming chat, structured outputs, and media generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
".": {
|
|
25
25
|
"types": "./dist/index.d.ts",
|
|
26
26
|
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./ui": {
|
|
29
|
+
"solid": "./src/ui.ts",
|
|
30
|
+
"types": "./src/ui.ts",
|
|
31
|
+
"import": "./src/ui.ts"
|
|
27
32
|
}
|
|
28
33
|
},
|
|
29
34
|
"files": [
|
|
@@ -45,11 +50,16 @@
|
|
|
45
50
|
"media-generation"
|
|
46
51
|
],
|
|
47
52
|
"dependencies": {
|
|
48
|
-
"
|
|
53
|
+
"rehype-highlight": "^7.0.2",
|
|
54
|
+
"rehype-raw": "^7.0.0",
|
|
55
|
+
"rehype-sanitize": "^6.0.0",
|
|
56
|
+
"remark-gfm": "^4.0.1",
|
|
57
|
+
"solid-markdown": "^2.1.0",
|
|
58
|
+
"@tanstack/ai-client": "^0.30.0"
|
|
49
59
|
},
|
|
50
60
|
"peerDependencies": {
|
|
51
61
|
"solid-js": ">=1.9.10",
|
|
52
|
-
"@tanstack/ai": "^0.
|
|
62
|
+
"@tanstack/ai": "^0.52.1"
|
|
53
63
|
},
|
|
54
64
|
"devDependencies": {
|
|
55
65
|
"@solidjs/testing-library": "^0.8.10",
|
|
@@ -60,8 +70,10 @@
|
|
|
60
70
|
"solid-js": "^1.9.10",
|
|
61
71
|
"tsdown": "^0.17.0-beta.6",
|
|
62
72
|
"typescript": "5.9.3",
|
|
73
|
+
"vite": "^8.2.1",
|
|
74
|
+
"vite-plugin-solid": "^2.11.14",
|
|
63
75
|
"vitest": "^4.1.10",
|
|
64
|
-
"@tanstack/ai": "0.
|
|
76
|
+
"@tanstack/ai": "0.52.1"
|
|
65
77
|
},
|
|
66
78
|
"scripts": {
|
|
67
79
|
"clean": "premove ./build ./dist",
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createSignal } from 'solid-js'
|
|
2
|
+
import { useChatContext } from './chat'
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
export interface ChatInputRenderProps {
|
|
6
|
+
/** Current input value */
|
|
7
|
+
value: string
|
|
8
|
+
/** Set input value */
|
|
9
|
+
onChange: (value: string) => void
|
|
10
|
+
/** Submit the message */
|
|
11
|
+
onSubmit: () => void
|
|
12
|
+
/** Is the chat currently loading */
|
|
13
|
+
isLoading: boolean
|
|
14
|
+
/** Is input disabled */
|
|
15
|
+
disabled: boolean
|
|
16
|
+
/** Ref callback for the input element */
|
|
17
|
+
ref: (el: HTMLInputElement | HTMLTextAreaElement) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @deprecated Use `createChatUI()` and an application-owned input component. Deprecated in 0.8.0. Removed in 1.0.0. */
|
|
21
|
+
export interface ChatInputProps {
|
|
22
|
+
/** Render prop for full control */
|
|
23
|
+
children?: (props: ChatInputRenderProps) => JSX.Element
|
|
24
|
+
/** CSS class name */
|
|
25
|
+
class?: string
|
|
26
|
+
/** Placeholder text */
|
|
27
|
+
placeholder?: string
|
|
28
|
+
/** Disable input */
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
/** Submit on Enter (Shift+Enter for new line) */
|
|
31
|
+
submitOnEnter?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use `createChatUI()` and an application-owned input component. Deprecated in 0.8.0. Removed in 1.0.0.
|
|
36
|
+
*
|
|
37
|
+
* Chat input component - handles message input and submission
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Auto-growing textarea
|
|
41
|
+
* - Submit on Enter (Shift+Enter for new line)
|
|
42
|
+
* - Loading state management
|
|
43
|
+
* - Full render prop support for custom UIs
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* <Chat.Input placeholder="Type your message..." />
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example Custom UI with render prop
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <Chat.Input>
|
|
53
|
+
* {({ value, onChange, onSubmit, isLoading }) => (
|
|
54
|
+
* <div>
|
|
55
|
+
* <textarea value={value} onInput={(e) => onChange(e.target.value)} />
|
|
56
|
+
* <button onClick={onSubmit} disabled={isLoading}>Send</button>
|
|
57
|
+
* </div>
|
|
58
|
+
* )}
|
|
59
|
+
* </Chat.Input>
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function ChatInput(props: ChatInputProps) {
|
|
63
|
+
const { sendMessage, isLoading } = useChatContext()
|
|
64
|
+
const [value, setValue] = createSignal('')
|
|
65
|
+
|
|
66
|
+
const disabled = () => props.disabled || isLoading()
|
|
67
|
+
|
|
68
|
+
const handleSubmit = () => {
|
|
69
|
+
if (!value().trim() || disabled()) return
|
|
70
|
+
void sendMessage(value())
|
|
71
|
+
setValue('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const renderProps = (): ChatInputRenderProps => ({
|
|
75
|
+
value: value(),
|
|
76
|
+
onChange: setValue,
|
|
77
|
+
onSubmit: handleSubmit,
|
|
78
|
+
isLoading: isLoading(),
|
|
79
|
+
disabled: disabled(),
|
|
80
|
+
|
|
81
|
+
ref: () => {},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Render prop pattern
|
|
85
|
+
if (props.children) {
|
|
86
|
+
return <>{props.children(renderProps())}</>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Default implementation
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
class={props.class}
|
|
93
|
+
data-chat-input
|
|
94
|
+
style={{
|
|
95
|
+
display: 'flex',
|
|
96
|
+
gap: '0.75rem',
|
|
97
|
+
'align-items': 'center',
|
|
98
|
+
width: '100%',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<input
|
|
102
|
+
type="text"
|
|
103
|
+
value={value()}
|
|
104
|
+
onInput={(e) => setValue(e.currentTarget.value)}
|
|
105
|
+
onKeyDown={(e) => {
|
|
106
|
+
if ((props.submitOnEnter ?? true) && e.key === 'Enter') {
|
|
107
|
+
e.preventDefault()
|
|
108
|
+
handleSubmit()
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
111
|
+
placeholder={props.placeholder ?? 'Type a message...'}
|
|
112
|
+
disabled={disabled()}
|
|
113
|
+
data-chat-textarea
|
|
114
|
+
style={{
|
|
115
|
+
flex: 1,
|
|
116
|
+
padding: '0.75rem 1rem',
|
|
117
|
+
'font-size': '0.875rem',
|
|
118
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
119
|
+
'border-radius': '0.75rem',
|
|
120
|
+
'background-color': 'rgba(31, 41, 55, 0.5)',
|
|
121
|
+
color: 'white',
|
|
122
|
+
outline: 'none',
|
|
123
|
+
transition: 'all 0.2s',
|
|
124
|
+
}}
|
|
125
|
+
onFocus={(e) => {
|
|
126
|
+
e.currentTarget.style.borderColor = 'rgba(249, 115, 22, 0.4)'
|
|
127
|
+
e.currentTarget.style.boxShadow = '0 0 0 2px rgba(249, 115, 22, 0.2)'
|
|
128
|
+
}}
|
|
129
|
+
onBlur={(e) => {
|
|
130
|
+
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)'
|
|
131
|
+
e.currentTarget.style.boxShadow = 'none'
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
<button
|
|
135
|
+
onClick={handleSubmit}
|
|
136
|
+
disabled={disabled() || !value().trim()}
|
|
137
|
+
data-chat-submit
|
|
138
|
+
style={{
|
|
139
|
+
padding: '0.75rem 1.5rem',
|
|
140
|
+
'font-size': '0.875rem',
|
|
141
|
+
'font-weight': 500,
|
|
142
|
+
color: 'white',
|
|
143
|
+
'background-color':
|
|
144
|
+
disabled() || !value().trim()
|
|
145
|
+
? 'rgba(107, 114, 128, 0.5)'
|
|
146
|
+
: 'rgb(249, 115, 22)',
|
|
147
|
+
border: 'none',
|
|
148
|
+
'border-radius': '0.75rem',
|
|
149
|
+
cursor: disabled() || !value().trim() ? 'not-allowed' : 'pointer',
|
|
150
|
+
transition: 'all 0.2s',
|
|
151
|
+
'white-space': 'nowrap',
|
|
152
|
+
}}
|
|
153
|
+
onMouseEnter={(e) => {
|
|
154
|
+
if (!disabled() && value().trim()) {
|
|
155
|
+
e.currentTarget.style.backgroundColor = 'rgb(234, 88, 12)'
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
onMouseLeave={(e) => {
|
|
159
|
+
if (!disabled() && value().trim()) {
|
|
160
|
+
e.currentTarget.style.backgroundColor = 'rgb(249, 115, 22)'
|
|
161
|
+
}
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{isLoading() ? 'Sending...' : 'Send'}
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js'
|
|
2
|
+
import { ThinkingPart } from './thinking-part'
|
|
3
|
+
import type { JSX } from 'solid-js'
|
|
4
|
+
import type { UIMessage } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface ToolCallRenderProps {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
arguments: string
|
|
10
|
+
state: string
|
|
11
|
+
approval?: any
|
|
12
|
+
output?: any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @deprecated Use `createChatUI()` Message instead. Deprecated in 0.8.0. Removed in 1.0.0. */
|
|
16
|
+
export interface ChatMessageProps {
|
|
17
|
+
/** The message to render */
|
|
18
|
+
message: UIMessage
|
|
19
|
+
/** Base CSS class name */
|
|
20
|
+
class?: string
|
|
21
|
+
/** Additional class for user messages */
|
|
22
|
+
userClass?: string
|
|
23
|
+
/** Additional class for assistant messages */
|
|
24
|
+
assistantClass?: string
|
|
25
|
+
/** Custom renderer for text parts */
|
|
26
|
+
textPartRenderer?: (props: { content: string }) => JSX.Element
|
|
27
|
+
/** Custom renderer for thinking parts */
|
|
28
|
+
thinkingPartRenderer?: (props: {
|
|
29
|
+
content: string
|
|
30
|
+
isComplete?: boolean
|
|
31
|
+
}) => JSX.Element
|
|
32
|
+
/** Named tool renderers - use the tool name as the key */
|
|
33
|
+
toolsRenderer?: Record<string, (props: ToolCallRenderProps) => JSX.Element>
|
|
34
|
+
/** Default tool renderer when tool name not found in toolsRenderer */
|
|
35
|
+
defaultToolRenderer?: (props: ToolCallRenderProps) => JSX.Element
|
|
36
|
+
/** Custom renderer for tool result parts */
|
|
37
|
+
toolResultRenderer?: (props: {
|
|
38
|
+
toolCallId: string
|
|
39
|
+
content: string
|
|
40
|
+
state: string
|
|
41
|
+
}) => JSX.Element
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated Use `createChatUI()` Message instead. Deprecated in 0.8.0. Removed in 1.0.0.
|
|
46
|
+
*
|
|
47
|
+
* Message component - renders a single message with all its parts
|
|
48
|
+
*
|
|
49
|
+
* This component natively understands TanStack AI's parts-based message format:
|
|
50
|
+
* - thinking parts: rendered as collapsible thinking/reasoning sections (auto-collapses when complete)
|
|
51
|
+
* - text parts: rendered as content
|
|
52
|
+
* - tool-call parts: rendered with state, approvals, etc.
|
|
53
|
+
* - tool-result parts: rendered with results
|
|
54
|
+
*
|
|
55
|
+
* @example Basic usage
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <Chat.Message message={message} />
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example With role-based styling
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <ChatMessage
|
|
63
|
+
* message={message}
|
|
64
|
+
* class="flex"
|
|
65
|
+
* userClass="justify-end"
|
|
66
|
+
* assistantClass="justify-start"
|
|
67
|
+
* />
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example With custom thinking renderer
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <ChatMessage
|
|
73
|
+
* message={message}
|
|
74
|
+
* thinkingPartRenderer={({ content, isComplete }) => (
|
|
75
|
+
* <details open={!isComplete}>
|
|
76
|
+
* <summary>Thinking...</summary>
|
|
77
|
+
* <pre>{content}</pre>
|
|
78
|
+
* </details>
|
|
79
|
+
* )}
|
|
80
|
+
* />
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example With named tool renderers
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <ChatMessage
|
|
86
|
+
* message={message}
|
|
87
|
+
* toolsRenderer={{
|
|
88
|
+
* recommendGuitar: ({ id, arguments: args }) => <GuitarCard {...JSON.parse(args)} />,
|
|
89
|
+
* weatherLookup: ({ id, arguments: args }) => <WeatherWidget {...JSON.parse(args)} />,
|
|
90
|
+
* }}
|
|
91
|
+
* defaultToolRenderer={() => null}
|
|
92
|
+
* />
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function ChatMessage(props: ChatMessageProps) {
|
|
96
|
+
// Combine classes based on role
|
|
97
|
+
const roleClass = () =>
|
|
98
|
+
props.message.role === 'user'
|
|
99
|
+
? (props.userClass ?? '')
|
|
100
|
+
: (props.assistantClass ?? '')
|
|
101
|
+
|
|
102
|
+
const combinedClass = () =>
|
|
103
|
+
[props.class ?? '', roleClass()].filter(Boolean).join(' ')
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
class={combinedClass() || undefined}
|
|
108
|
+
data-message-id={props.message.id}
|
|
109
|
+
data-message-role={props.message.role}
|
|
110
|
+
data-message-created={props.message.createdAt?.toISOString()}
|
|
111
|
+
>
|
|
112
|
+
<For each={props.message.parts}>
|
|
113
|
+
{(part, index) => {
|
|
114
|
+
// Check if thinking is complete (if there's a text part after this thinking part)
|
|
115
|
+
const isThinkingComplete = () =>
|
|
116
|
+
part.type === 'thinking' &&
|
|
117
|
+
props.message.parts
|
|
118
|
+
.slice(index() + 1)
|
|
119
|
+
.some((p) => p.type === 'text')
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<MessagePart
|
|
123
|
+
part={part}
|
|
124
|
+
isThinkingComplete={isThinkingComplete()}
|
|
125
|
+
textPartRenderer={props.textPartRenderer}
|
|
126
|
+
thinkingPartRenderer={props.thinkingPartRenderer}
|
|
127
|
+
toolsRenderer={props.toolsRenderer}
|
|
128
|
+
defaultToolRenderer={props.defaultToolRenderer}
|
|
129
|
+
toolResultRenderer={props.toolResultRenderer}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
}}
|
|
133
|
+
</For>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function MessagePart(props: {
|
|
139
|
+
part: any
|
|
140
|
+
isThinkingComplete?: boolean
|
|
141
|
+
textPartRenderer?: ChatMessageProps['textPartRenderer']
|
|
142
|
+
thinkingPartRenderer?: ChatMessageProps['thinkingPartRenderer']
|
|
143
|
+
toolsRenderer?: ChatMessageProps['toolsRenderer']
|
|
144
|
+
defaultToolRenderer?: ChatMessageProps['defaultToolRenderer']
|
|
145
|
+
toolResultRenderer?: ChatMessageProps['toolResultRenderer']
|
|
146
|
+
}) {
|
|
147
|
+
// Text part
|
|
148
|
+
if (props.part.type === 'text') {
|
|
149
|
+
if (props.textPartRenderer) {
|
|
150
|
+
return <>{props.textPartRenderer({ content: props.part.content })}</>
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
<div data-part-type="text" data-part-content>
|
|
154
|
+
{props.part.content}
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Thinking part
|
|
160
|
+
if (props.part.type === 'thinking') {
|
|
161
|
+
if (props.thinkingPartRenderer) {
|
|
162
|
+
return (
|
|
163
|
+
<>
|
|
164
|
+
{props.thinkingPartRenderer({
|
|
165
|
+
content: props.part.content,
|
|
166
|
+
isComplete: props.isThinkingComplete,
|
|
167
|
+
})}
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
return (
|
|
172
|
+
<ThinkingPart
|
|
173
|
+
content={props.part.content}
|
|
174
|
+
isComplete={props.isThinkingComplete}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Tool call part
|
|
180
|
+
if (props.part.type === 'tool-call') {
|
|
181
|
+
const toolProps: ToolCallRenderProps = {
|
|
182
|
+
id: props.part.id,
|
|
183
|
+
name: props.part.name,
|
|
184
|
+
arguments: props.part.arguments,
|
|
185
|
+
state: props.part.state,
|
|
186
|
+
approval: props.part.approval,
|
|
187
|
+
output: props.part.output,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if there's a specific renderer for this tool
|
|
191
|
+
if (props.toolsRenderer?.[props.part.name]) {
|
|
192
|
+
return <>{props.toolsRenderer[props.part.name]?.(toolProps)}</>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Use default tool renderer if provided
|
|
196
|
+
if (props.defaultToolRenderer) {
|
|
197
|
+
return <>{props.defaultToolRenderer(toolProps)}</>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Fallback to built-in default renderer
|
|
201
|
+
return (
|
|
202
|
+
<div
|
|
203
|
+
data-part-type="tool-call"
|
|
204
|
+
data-tool-name={props.part.name}
|
|
205
|
+
data-tool-state={props.part.state}
|
|
206
|
+
data-tool-id={props.part.id}
|
|
207
|
+
>
|
|
208
|
+
<div data-tool-header>
|
|
209
|
+
<strong>{props.part.name}</strong>
|
|
210
|
+
<span data-tool-state-badge>{props.part.state}</span>
|
|
211
|
+
</div>
|
|
212
|
+
<Show when={props.part.arguments}>
|
|
213
|
+
<div data-tool-arguments>
|
|
214
|
+
<pre>{props.part.arguments}</pre>
|
|
215
|
+
</div>
|
|
216
|
+
</Show>
|
|
217
|
+
<Show when={props.part.approval}>
|
|
218
|
+
<div data-tool-approval>
|
|
219
|
+
{props.part.approval.approved !== undefined
|
|
220
|
+
? props.part.approval.approved
|
|
221
|
+
? '✓ Approved'
|
|
222
|
+
: '✗ Denied'
|
|
223
|
+
: '⏳ Awaiting approval...'}
|
|
224
|
+
</div>
|
|
225
|
+
</Show>
|
|
226
|
+
<Show when={props.part.output}>
|
|
227
|
+
<div data-tool-output>
|
|
228
|
+
<pre>{JSON.stringify(props.part.output, null, 2)}</pre>
|
|
229
|
+
</div>
|
|
230
|
+
</Show>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Tool result part
|
|
236
|
+
if (props.part.type === 'tool-result') {
|
|
237
|
+
if (props.toolResultRenderer) {
|
|
238
|
+
return (
|
|
239
|
+
<>
|
|
240
|
+
{props.toolResultRenderer({
|
|
241
|
+
toolCallId: props.part.toolCallId,
|
|
242
|
+
content: props.part.content,
|
|
243
|
+
state: props.part.state,
|
|
244
|
+
})}
|
|
245
|
+
</>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
data-part-type="tool-result"
|
|
252
|
+
data-tool-call-id={props.part.toolCallId}
|
|
253
|
+
data-tool-result-state={props.part.state}
|
|
254
|
+
>
|
|
255
|
+
<div data-tool-result-content>{props.part.content}</div>
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { For, Show, createEffect } from 'solid-js'
|
|
2
|
+
import { useChatContext } from './chat'
|
|
3
|
+
import { ChatMessage } from './chat-message'
|
|
4
|
+
import type { JSX } from 'solid-js'
|
|
5
|
+
import type { UIMessage } from '../types'
|
|
6
|
+
|
|
7
|
+
/** @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.8.0. Removed in 1.0.0. */
|
|
8
|
+
export interface ChatMessagesProps {
|
|
9
|
+
/** Custom render function for each message */
|
|
10
|
+
children?: (message: UIMessage, index: number) => JSX.Element
|
|
11
|
+
/** CSS class name */
|
|
12
|
+
class?: string
|
|
13
|
+
/** Element to show when there are no messages */
|
|
14
|
+
emptyState?: JSX.Element
|
|
15
|
+
/** Element to show while loading the first message */
|
|
16
|
+
loadingState?: JSX.Element
|
|
17
|
+
/** Custom error renderer */
|
|
18
|
+
errorState?: (props: {
|
|
19
|
+
error: Error
|
|
20
|
+
reload: () => Promise<void>
|
|
21
|
+
}) => JSX.Element
|
|
22
|
+
/** Auto-scroll to bottom on new messages */
|
|
23
|
+
autoScroll?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.8.0. Removed in 1.0.0.
|
|
28
|
+
*
|
|
29
|
+
* Messages container - renders all messages in the conversation
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <Chat.Messages>
|
|
34
|
+
* {(message) => <Chat.Message message={message} />}
|
|
35
|
+
* </Chat.Messages>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function ChatMessages(props: ChatMessagesProps) {
|
|
39
|
+
const { messages, isLoading, error, reload } = useChatContext()
|
|
40
|
+
let containerRef: HTMLDivElement | undefined
|
|
41
|
+
|
|
42
|
+
// Auto-scroll to bottom on new messages
|
|
43
|
+
createEffect(() => {
|
|
44
|
+
// Track messages to trigger effect on change
|
|
45
|
+
messages()
|
|
46
|
+
if ((props.autoScroll ?? true) && containerRef) {
|
|
47
|
+
containerRef.scrollTop = containerRef.scrollHeight
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Show
|
|
53
|
+
when={!error() || !props.errorState}
|
|
54
|
+
fallback={(() => {
|
|
55
|
+
const err = error()
|
|
56
|
+
return err ? <>{props.errorState?.({ error: err, reload })}</> : null
|
|
57
|
+
})()}
|
|
58
|
+
>
|
|
59
|
+
<Show
|
|
60
|
+
when={!isLoading() || messages().length > 0 || !props.loadingState}
|
|
61
|
+
fallback={<>{props.loadingState}</>}
|
|
62
|
+
>
|
|
63
|
+
<Show
|
|
64
|
+
when={messages().length > 0 || !props.emptyState}
|
|
65
|
+
fallback={<>{props.emptyState}</>}
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
ref={(el) => {
|
|
69
|
+
containerRef = el
|
|
70
|
+
}}
|
|
71
|
+
class={props.class}
|
|
72
|
+
data-chat-messages
|
|
73
|
+
data-message-count={messages().length}
|
|
74
|
+
>
|
|
75
|
+
<For each={messages()}>
|
|
76
|
+
{(message, index) =>
|
|
77
|
+
props.children ? (
|
|
78
|
+
<div data-message-id={message.id}>
|
|
79
|
+
{props.children(message, index())}
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<ChatMessage message={message} />
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
</For>
|
|
86
|
+
</div>
|
|
87
|
+
</Show>
|
|
88
|
+
</Show>
|
|
89
|
+
</Show>
|
|
90
|
+
)
|
|
91
|
+
}
|