@tulip-systems/ai 0.11.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/LICENSE +662 -0
- package/dist/agents/lib/definitions.server.d.mts +11 -0
- package/dist/agents/lib/definitions.server.mjs +14 -0
- package/dist/agents/lib/delegation.server.d.mts +15 -0
- package/dist/agents/lib/delegation.server.mjs +28 -0
- package/dist/agents/lib/instructions.server.d.mts +14 -0
- package/dist/agents/lib/instructions.server.mjs +26 -0
- package/dist/agents/lib/types.d.mts +26 -0
- package/dist/agents/server.d.mts +5 -0
- package/dist/agents/server.mjs +5 -0
- package/dist/chat/client.d.mts +10 -0
- package/dist/chat/client.mjs +12 -0
- package/dist/chat/components/agent-activity.client.d.mts +14 -0
- package/dist/chat/components/agent-activity.client.mjs +26 -0
- package/dist/chat/components/assistant-panel.client.d.mts +40 -0
- package/dist/chat/components/assistant-panel.client.mjs +53 -0
- package/dist/chat/components/chat-composer.client.d.mts +160 -0
- package/dist/chat/components/chat-composer.client.mjs +266 -0
- package/dist/chat/components/chat-message.client.d.mts +92 -0
- package/dist/chat/components/chat-message.client.mjs +121 -0
- package/dist/chat/components/chat-thread.client.d.mts +130 -0
- package/dist/chat/components/chat-thread.client.mjs +164 -0
- package/dist/chat/components/message-part.client.d.mts +23 -0
- package/dist/chat/components/message-part.client.mjs +38 -0
- package/dist/chat/components/starter-prompts.client.d.mts +25 -0
- package/dist/chat/components/starter-prompts.client.mjs +31 -0
- package/dist/chat/components/tool-call-card.client.d.mts +29 -0
- package/dist/chat/components/tool-call-card.client.mjs +60 -0
- package/dist/chat/lib/classes.mjs +7 -0
- package/dist/chat/lib/parts.d.mts +10 -0
- package/dist/chat/lib/parts.mjs +25 -0
- package/dist/chat/lib/stream.server.d.mts +24 -0
- package/dist/chat/lib/stream.server.mjs +34 -0
- package/dist/chat/lib/types.d.mts +21 -0
- package/dist/chat/server.d.mts +3 -0
- package/dist/chat/server.mjs +3 -0
- package/dist/chat.d.mts +3 -0
- package/dist/chat.mjs +3 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/mcp/lib/content.d.mts +66 -0
- package/dist/mcp/lib/content.d.mts.map +1 -0
- package/dist/mcp/lib/content.mjs +115 -0
- package/dist/mcp/lib/content.mjs.map +1 -0
- package/dist/mcp/lib/tool-result.server.d.mts +12 -0
- package/dist/mcp/lib/tool-result.server.d.mts.map +1 -0
- package/dist/mcp/lib/tool-result.server.mjs +10 -0
- package/dist/mcp/lib/tool-result.server.mjs.map +1 -0
- package/dist/mcp/lib/types.d.mts +27 -0
- package/dist/mcp/server.d.mts +3 -0
- package/dist/mcp/server.mjs +3 -0
- package/dist/mcp/types.d.mts +27 -0
- package/dist/mcp/types.d.mts.map +1 -0
- package/dist/server/mcp-content.d.mts +44 -0
- package/dist/server/mcp-content.d.mts.map +1 -0
- package/dist/server/mcp-content.mjs +71 -0
- package/dist/server/mcp-content.mjs.map +1 -0
- package/dist/server/tools.d.mts +61 -0
- package/dist/server/tools.d.mts.map +1 -0
- package/dist/server/tools.mjs +47 -0
- package/dist/server/tools.mjs.map +1 -0
- package/dist/server.d.mts +3 -0
- package/dist/server.mjs +4 -0
- package/dist/tools/client.d.mts +3 -0
- package/dist/tools/client.mjs +5 -0
- package/dist/tools/lib/ai-sdk.server.d.mts +12 -0
- package/dist/tools/lib/ai-sdk.server.d.mts.map +1 -0
- package/dist/tools/lib/ai-sdk.server.mjs +38 -0
- package/dist/tools/lib/ai-sdk.server.mjs.map +1 -0
- package/dist/tools/lib/define-tool.server.d.mts +9 -0
- package/dist/tools/lib/define-tool.server.d.mts.map +1 -0
- package/dist/tools/lib/define-tool.server.mjs +13 -0
- package/dist/tools/lib/define-tool.server.mjs.map +1 -0
- package/dist/tools/lib/definitions.client.d.mts +20 -0
- package/dist/tools/lib/definitions.client.mjs +21 -0
- package/dist/tools/lib/definitions.d.mts +16 -0
- package/dist/tools/lib/definitions.d.mts.map +1 -0
- package/dist/tools/lib/definitions.mjs +20 -0
- package/dist/tools/lib/definitions.mjs.map +1 -0
- package/dist/tools/lib/resolvers.d.mts +41 -0
- package/dist/tools/lib/resolvers.d.mts.map +1 -0
- package/dist/tools/lib/resolvers.mjs +43 -0
- package/dist/tools/lib/resolvers.mjs.map +1 -0
- package/dist/tools/lib/types.client.d.mts +13 -0
- package/dist/tools/lib/types.d.mts +33 -0
- package/dist/tools/resolvers/agent.server.d.mts +27 -0
- package/dist/tools/resolvers/agent.server.d.mts.map +1 -0
- package/dist/tools/resolvers/agent.server.mjs +44 -0
- package/dist/tools/resolvers/agent.server.mjs.map +1 -0
- package/dist/tools/resolvers/mcp.server.d.mts +31 -0
- package/dist/tools/resolvers/mcp.server.d.mts.map +1 -0
- package/dist/tools/resolvers/mcp.server.mjs +20 -0
- package/dist/tools/resolvers/mcp.server.mjs.map +1 -0
- package/dist/tools/server.d.mts +5 -0
- package/dist/tools/server.mjs +5 -0
- package/dist/tools/types.d.mts +33 -0
- package/dist/tools/types.d.mts.map +1 -0
- package/package.json +61 -0
- package/src/agents/entry.server.ts +7 -0
- package/src/agents/lib/definitions.server.ts +14 -0
- package/src/agents/lib/delegation.server.ts +33 -0
- package/src/agents/lib/instructions.server.ts +37 -0
- package/src/agents/lib/types.ts +32 -0
- package/src/chat/components/agent-activity.client.tsx +21 -0
- package/src/chat/components/chat-composer.client.tsx +379 -0
- package/src/chat/components/chat-message.client.tsx +166 -0
- package/src/chat/components/chat-thread.client.tsx +242 -0
- package/src/chat/components/message-part.client.tsx +59 -0
- package/src/chat/components/starter-prompts.client.tsx +49 -0
- package/src/chat/components/tool-call-card.client.tsx +78 -0
- package/src/chat/entry.client.ts +18 -0
- package/src/chat/entry.server.ts +5 -0
- package/src/chat/entry.ts +5 -0
- package/src/chat/lib/parts.ts +33 -0
- package/src/chat/lib/stream.server.ts +40 -0
- package/src/chat/lib/types.ts +20 -0
- package/src/entry.ts +24 -0
- package/src/mcp/entry.server.ts +2 -0
- package/src/mcp/lib/content.ts +131 -0
- package/src/mcp/lib/types.ts +28 -0
- package/src/styles.css +1 -0
- package/src/tools/entry.client.ts +7 -0
- package/src/tools/entry.server.ts +11 -0
- package/src/tools/lib/definitions.client.ts +21 -0
- package/src/tools/lib/definitions.ts +24 -0
- package/src/tools/lib/types.client.ts +11 -0
- package/src/tools/lib/types.ts +44 -0
- package/src/tools/resolvers/agent.server.ts +78 -0
- package/src/tools/resolvers/mcp.server.ts +40 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button, Textarea } from "@tulip-systems/core/components";
|
|
4
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
5
|
+
import type { ChatStatus } from "ai";
|
|
6
|
+
import { ArrowUp, UploadIcon } from "lucide-react";
|
|
7
|
+
import {
|
|
8
|
+
type ComponentProps,
|
|
9
|
+
createContext,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
useCallback,
|
|
12
|
+
useContext,
|
|
13
|
+
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useRef,
|
|
16
|
+
} from "react";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Chat Composer Context
|
|
20
|
+
*/
|
|
21
|
+
type ChatComposerContextValue = {
|
|
22
|
+
value: string;
|
|
23
|
+
onValueChange: (value: string) => void;
|
|
24
|
+
submit: () => void;
|
|
25
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
canSubmit: boolean;
|
|
28
|
+
status: ChatStatus;
|
|
29
|
+
statusLabel?: ReactNode;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ChatComposerContext = createContext<ChatComposerContextValue>({} as ChatComposerContextValue);
|
|
33
|
+
|
|
34
|
+
export type ChatComposerProps = Omit<ComponentProps<"form">, "onSubmit"> & {
|
|
35
|
+
value: string;
|
|
36
|
+
onValueChange: (value: string) => void;
|
|
37
|
+
onSubmit: (value: string) => void;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
status?: ChatStatus;
|
|
40
|
+
statusLabel?: ReactNode;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Provides the root composer shell and shared state for all chat composer slots.
|
|
45
|
+
* Use it as the controlled boundary for the current draft value and submit handler.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <ChatComposer value={input} onValueChange={setInput} onSubmit={sendMessage}>
|
|
50
|
+
* <ChatComposerInput placeholder="Ask something..." />
|
|
51
|
+
* <ChatComposerToolbar>
|
|
52
|
+
* <ChatComposerActions>
|
|
53
|
+
* <ChatComposerAttachmentButton />
|
|
54
|
+
* </ChatComposerActions>
|
|
55
|
+
* <ChatComposerSubmit />
|
|
56
|
+
* </ChatComposerToolbar>
|
|
57
|
+
* </ChatComposer>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function ChatComposer({
|
|
61
|
+
value,
|
|
62
|
+
onValueChange,
|
|
63
|
+
onSubmit,
|
|
64
|
+
disabled = false,
|
|
65
|
+
status = "ready",
|
|
66
|
+
statusLabel,
|
|
67
|
+
className,
|
|
68
|
+
children,
|
|
69
|
+
...props
|
|
70
|
+
}: ChatComposerProps) {
|
|
71
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
72
|
+
const isDisabled = disabled;
|
|
73
|
+
const isSubmitDisabled = disabled || status !== "ready";
|
|
74
|
+
const canSubmit = !isSubmitDisabled && value.trim().length > 0;
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
adjustTextareaHeight(textareaRef.current);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const submit = useCallback(() => {
|
|
81
|
+
const trimmedValue = value.trim();
|
|
82
|
+
|
|
83
|
+
if (!trimmedValue || isSubmitDisabled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onSubmit(trimmedValue);
|
|
88
|
+
resetTextareaHeight(textareaRef.current);
|
|
89
|
+
}, [value, isSubmitDisabled, onSubmit]);
|
|
90
|
+
|
|
91
|
+
const contextValue = useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
value,
|
|
94
|
+
onValueChange,
|
|
95
|
+
submit,
|
|
96
|
+
textareaRef,
|
|
97
|
+
disabled,
|
|
98
|
+
canSubmit,
|
|
99
|
+
status,
|
|
100
|
+
statusLabel,
|
|
101
|
+
}),
|
|
102
|
+
[value, onValueChange, submit, disabled, canSubmit, status, statusLabel],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<ChatComposerContext.Provider value={contextValue}>
|
|
107
|
+
<form className={cn("pointer-events-auto mx-auto w-full max-w-3xl", className)} {...props}>
|
|
108
|
+
<fieldset
|
|
109
|
+
data-disabled={isDisabled ? true : undefined}
|
|
110
|
+
className="relative flex min-h-28 w-full min-w-0 flex-col rounded-xl border border-input/70 bg-background/80 shadow-[0_12px_40px_rgba(0,0,0,0.08)] outline-none backdrop-blur-xl transition-colors focus-within:border-primary/35 focus-within:ring-3 focus-within:ring-ring/50 data-[disabled=true]:opacity-70"
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</fieldset>
|
|
114
|
+
</form>
|
|
115
|
+
</ChatComposerContext.Provider>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Renders the auto-growing textarea connected to the nearest `ChatComposer`.
|
|
121
|
+
* Pressing Enter submits the composer, while Shift+Enter inserts a new line.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```tsx
|
|
125
|
+
* <ChatComposerInput placeholder="Ask something..." rows={3} />
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function ChatComposerInput({
|
|
129
|
+
rows = 3,
|
|
130
|
+
className,
|
|
131
|
+
onKeyDown,
|
|
132
|
+
...props
|
|
133
|
+
}: Omit<ComponentProps<typeof Textarea>, "value" | "onChange" | "disabled">) {
|
|
134
|
+
const { value, onValueChange, submit, textareaRef, disabled } = useContext(ChatComposerContext);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Textarea
|
|
138
|
+
ref={textareaRef}
|
|
139
|
+
value={value}
|
|
140
|
+
onChange={(event) => {
|
|
141
|
+
onValueChange(event.target.value);
|
|
142
|
+
adjustTextareaHeight(event.target);
|
|
143
|
+
}}
|
|
144
|
+
onKeyDown={(event) => {
|
|
145
|
+
onKeyDown?.(event);
|
|
146
|
+
|
|
147
|
+
if (event.defaultPrevented) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
submit();
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
disabled={disabled}
|
|
157
|
+
rows={rows}
|
|
158
|
+
className={cn(
|
|
159
|
+
"max-h-[calc(75dvh)] min-h-20 flex-1 resize-none overflow-hidden rounded-none border-0 bg-transparent px-3 pt-3 pb-0 text-base leading-6 shadow-none ring-0 placeholder:text-muted-foreground/70 focus-visible:ring-0 disabled:bg-transparent dark:bg-transparent dark:disabled:bg-transparent",
|
|
160
|
+
className,
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Renders the bottom toolbar area for composer actions and the submit button.
|
|
169
|
+
* Place `ChatComposerActions` on the left and `ChatComposerSubmit` on the right.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```tsx
|
|
173
|
+
* <ChatComposerToolbar>
|
|
174
|
+
* <ChatComposerActions>
|
|
175
|
+
* <ChatComposerAttachmentButton />
|
|
176
|
+
* </ChatComposerActions>
|
|
177
|
+
* <ChatComposerSubmit />
|
|
178
|
+
* </ChatComposerToolbar>
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function ChatComposerToolbar({ className, ...props }: ComponentProps<"div">) {
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
className={cn(
|
|
185
|
+
"flex w-full items-center justify-between gap-2 px-2.5 pt-1 pb-2 text-muted-foreground text-sm",
|
|
186
|
+
className,
|
|
187
|
+
)}
|
|
188
|
+
{...props}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Groups secondary composer controls such as attachments, model selection, or status.
|
|
195
|
+
* It keeps action controls aligned together inside `ChatComposerToolbar`.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```tsx
|
|
199
|
+
* <ChatComposerActions>
|
|
200
|
+
* <ChatComposerAttachmentButton />
|
|
201
|
+
* <ChatComposerModelSelect>GPT-5.5</ChatComposerModelSelect>
|
|
202
|
+
* <ChatComposerStatus />
|
|
203
|
+
* </ChatComposerActions>
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function ChatComposerActions({ className, ...props }: ComponentProps<"div">) {
|
|
207
|
+
return <div className={cn("flex min-w-0 items-center gap-2", className)} {...props} />;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Renders an attachment action button for opening a file picker or context picker.
|
|
212
|
+
* It uses a file-upload icon and `Context toevoegen` accessible label by default.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```tsx
|
|
216
|
+
* <ChatComposerAttachmentButton onClick={openFilePicker} />
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export function ChatComposerAttachmentButton({
|
|
220
|
+
className,
|
|
221
|
+
disabled,
|
|
222
|
+
...props
|
|
223
|
+
}: ComponentProps<typeof Button>) {
|
|
224
|
+
const { disabled: composerDisabled } = useContext(ChatComposerContext);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<Button
|
|
228
|
+
aria-label="Bestanden toevoegen"
|
|
229
|
+
type="button"
|
|
230
|
+
size="icon"
|
|
231
|
+
variant="outline"
|
|
232
|
+
disabled={composerDisabled || disabled}
|
|
233
|
+
className={cn("size-8 rounded-lg p-0 shadow-none", className)}
|
|
234
|
+
{...props}
|
|
235
|
+
>
|
|
236
|
+
<UploadIcon className="size-4" />
|
|
237
|
+
<span className="sr-only">Bestanden toevoegen</span>
|
|
238
|
+
</Button>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Renders a compact model selector trigger inside the composer toolbar.
|
|
244
|
+
* Use it as a simple button or compose it with a popover/select in application code.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```tsx
|
|
248
|
+
* <ChatComposerModelSelect onClick={openModelMenu}>GPT-5.5</ChatComposerModelSelect>
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export function ChatComposerModelSelect({
|
|
252
|
+
className,
|
|
253
|
+
disabled,
|
|
254
|
+
children,
|
|
255
|
+
...props
|
|
256
|
+
}: ComponentProps<typeof Button>) {
|
|
257
|
+
const { disabled: composerDisabled } = useContext(ChatComposerContext);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Button
|
|
261
|
+
type="button"
|
|
262
|
+
variant="outline"
|
|
263
|
+
size="sm"
|
|
264
|
+
disabled={composerDisabled || disabled}
|
|
265
|
+
className={cn("h-8 rounded-lg shadow-none", className)}
|
|
266
|
+
{...props}
|
|
267
|
+
>
|
|
268
|
+
Select model
|
|
269
|
+
</Button>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Displays the current composer status from `ChatComposer.status` and `statusLabel`.
|
|
275
|
+
* It renders nothing when no label or children are provided.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```tsx
|
|
279
|
+
* <ChatComposer status="streaming" statusLabel="Thinking...">
|
|
280
|
+
* <ChatComposerInput />
|
|
281
|
+
* <ChatComposerToolbar>
|
|
282
|
+
* <ChatComposerActions>
|
|
283
|
+
* <ChatComposerStatus />
|
|
284
|
+
* </ChatComposerActions>
|
|
285
|
+
* <ChatComposerSubmit />
|
|
286
|
+
* </ChatComposerToolbar>
|
|
287
|
+
* </ChatComposer>
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
export function ChatComposerStatus({ className, children, ...props }: ComponentProps<"div">) {
|
|
291
|
+
const { status, statusLabel } = useContext(ChatComposerContext);
|
|
292
|
+
const content = children ?? statusLabel;
|
|
293
|
+
|
|
294
|
+
if (!content) return null;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div
|
|
298
|
+
className={cn(
|
|
299
|
+
"line-clamp-1 flex items-center gap-2 text-muted-foreground text-xs opacity-70 data-[status=ready]:opacity-100",
|
|
300
|
+
className,
|
|
301
|
+
)}
|
|
302
|
+
data-status={status}
|
|
303
|
+
{...props}
|
|
304
|
+
>
|
|
305
|
+
<span
|
|
306
|
+
className={cn(
|
|
307
|
+
"size-2 rounded-full",
|
|
308
|
+
status === "ready"
|
|
309
|
+
? "bg-emerald-500"
|
|
310
|
+
: status === "error"
|
|
311
|
+
? "bg-red-500"
|
|
312
|
+
: "bg-amber-500",
|
|
313
|
+
)}
|
|
314
|
+
/>
|
|
315
|
+
<span>{content}</span>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Renders the primary submit button for the current composer draft.
|
|
322
|
+
* It uses an arrow-up icon by default and disables itself while the composer cannot submit.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```tsx
|
|
326
|
+
* <ChatComposerSubmit />
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
export function ChatComposerSubmit({
|
|
330
|
+
className,
|
|
331
|
+
disabled,
|
|
332
|
+
onClick,
|
|
333
|
+
...props
|
|
334
|
+
}: ComponentProps<typeof Button>) {
|
|
335
|
+
const { canSubmit, submit, status } = useContext(ChatComposerContext);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<Button
|
|
339
|
+
type="button"
|
|
340
|
+
variant="default"
|
|
341
|
+
size="icon"
|
|
342
|
+
disabled={!canSubmit || disabled}
|
|
343
|
+
className={cn("size-8 rounded-full p-0 shadow-none", className)}
|
|
344
|
+
aria-label="Send message"
|
|
345
|
+
onClick={(event) => {
|
|
346
|
+
onClick?.(event);
|
|
347
|
+
|
|
348
|
+
if (!event.defaultPrevented) {
|
|
349
|
+
submit();
|
|
350
|
+
}
|
|
351
|
+
}}
|
|
352
|
+
{...props}
|
|
353
|
+
>
|
|
354
|
+
<ArrowUp className="size-4" />
|
|
355
|
+
<span className="sr-only">{status !== "ready" ? "Wait for response" : "Send message"}</span>
|
|
356
|
+
</Button>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Helpers
|
|
362
|
+
*/
|
|
363
|
+
function adjustTextareaHeight(textarea: HTMLTextAreaElement | null) {
|
|
364
|
+
if (!textarea) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
textarea.style.height = "auto";
|
|
369
|
+
textarea.style.height = `${textarea.scrollHeight + 2}px`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resetTextareaHeight(textarea: HTMLTextAreaElement | null) {
|
|
373
|
+
if (!textarea) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
textarea.style.height = "auto";
|
|
378
|
+
textarea.style.height = "8rem";
|
|
379
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Message,
|
|
5
|
+
MessageContent as MessageContentPrimitive,
|
|
6
|
+
MessageFooter as MessageFooterPrimitive,
|
|
7
|
+
MessageHeader as MessageHeaderPrimitive,
|
|
8
|
+
} from "@tulip-systems/core/components";
|
|
9
|
+
import { cn } from "@tulip-systems/core/lib";
|
|
10
|
+
import type { UIMessage } from "ai";
|
|
11
|
+
import { type ComponentProps, createContext, useContext, useMemo } from "react";
|
|
12
|
+
import type { ToolClientDefinition } from "../../tools/lib/types.client";
|
|
13
|
+
import { getChatPartKey, isToolPart } from "../lib/parts";
|
|
14
|
+
import type { ChatPart } from "../lib/types";
|
|
15
|
+
import { ChatMessagePart } from "./message-part.client";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Chat Message Context
|
|
19
|
+
*/
|
|
20
|
+
type ChatMessageContextValue = {
|
|
21
|
+
message: UIMessage;
|
|
22
|
+
isUser: boolean;
|
|
23
|
+
visibleParts: ChatPart[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ChatMessageContext = createContext<ChatMessageContextValue>({} as ChatMessageContextValue);
|
|
27
|
+
|
|
28
|
+
export type ChatMessageProps = ComponentProps<typeof Message> & {
|
|
29
|
+
message: UIMessage;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Provides the root message container and message context for all chat message slots.
|
|
34
|
+
* Use it with `ChatMessageContent` and `ChatMessageParts` for the default message layout.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <ChatMessage message={message}>
|
|
39
|
+
* <ChatMessageContent>
|
|
40
|
+
* <ChatMessageParts toolRenderers={toolRenderers} />
|
|
41
|
+
* </ChatMessageContent>
|
|
42
|
+
* </ChatMessage>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function ChatMessage({ message, children, ...props }: ChatMessageProps) {
|
|
46
|
+
const isUser = message.role === "user";
|
|
47
|
+
const contextValue = useMemo(() => {
|
|
48
|
+
const visibleParts = message.parts.filter((part) => part.type === "text" || isToolPart(part));
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
message,
|
|
52
|
+
isUser,
|
|
53
|
+
visibleParts,
|
|
54
|
+
};
|
|
55
|
+
}, [message, isUser]);
|
|
56
|
+
|
|
57
|
+
if (contextValue.visibleParts.length === 0) return null;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ChatMessageContext.Provider value={contextValue}>
|
|
61
|
+
<Message align={isUser ? "end" : "start"} {...props}>
|
|
62
|
+
{children}
|
|
63
|
+
</Message>
|
|
64
|
+
</ChatMessageContext.Provider>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Renders the content column for a chat message.
|
|
70
|
+
* Place `ChatMessageHeader`, `ChatMessageParts`, and `ChatMessageFooter` inside it.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <ChatMessageContent>
|
|
75
|
+
* <ChatMessageParts toolRenderers={toolRenderers} />
|
|
76
|
+
* </ChatMessageContent>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function ChatMessageContent({
|
|
80
|
+
className,
|
|
81
|
+
...props
|
|
82
|
+
}: ComponentProps<typeof MessageContentPrimitive>) {
|
|
83
|
+
const { isUser } = useContext(ChatMessageContext);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<MessageContentPrimitive
|
|
87
|
+
className={cn(isUser && "ml-auto w-fit max-w-[82%] md:max-w-[68%]", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Renders a message header slot.
|
|
95
|
+
* If no children are passed, it renders nothing.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* <ChatMessageHeader>Assistant</ChatMessageHeader>
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function ChatMessageHeader({ className, children, ...props }: ComponentProps<"div">) {
|
|
103
|
+
const { isUser } = useContext(ChatMessageContext);
|
|
104
|
+
|
|
105
|
+
if (!children) return null;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<MessageHeaderPrimitive className={cn("px-1", isUser && "justify-end", className)} {...props}>
|
|
109
|
+
{children}
|
|
110
|
+
</MessageHeaderPrimitive>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Renders the visible text and tool parts for the current chat message.
|
|
116
|
+
* Pass `toolRenderers` to customize how known tool calls/results are displayed.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* <ChatMessageParts toolRenderers={toolRenderers} />
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function ChatMessageParts({
|
|
124
|
+
toolRenderers,
|
|
125
|
+
className,
|
|
126
|
+
parts,
|
|
127
|
+
...props
|
|
128
|
+
}: ComponentProps<"div"> & {
|
|
129
|
+
toolRenderers?: ToolClientDefinition[];
|
|
130
|
+
parts?: ChatPart[];
|
|
131
|
+
}) {
|
|
132
|
+
const { message, isUser, visibleParts } = useContext(ChatMessageContext);
|
|
133
|
+
const resolvedParts = parts ?? visibleParts;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={cn("flex max-w-full flex-col gap-2.5", className)} {...props}>
|
|
137
|
+
{resolvedParts.map((part) => (
|
|
138
|
+
<ChatMessagePart
|
|
139
|
+
key={getChatPartKey(message.id, part)}
|
|
140
|
+
part={part}
|
|
141
|
+
isUser={isUser}
|
|
142
|
+
toolRenderers={toolRenderers}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Renders a message footer slot.
|
|
151
|
+
* If no children are passed, it renders nothing.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* <ChatMessageFooter>Copied</ChatMessageFooter>
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function ChatMessageFooter({ className, children, ...props }: ComponentProps<"div">) {
|
|
159
|
+
if (!children) return null;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<MessageFooterPrimitive className={cn("px-1", className)} {...props}>
|
|
163
|
+
{children}
|
|
164
|
+
</MessageFooterPrimitive>
|
|
165
|
+
);
|
|
166
|
+
}
|